普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月9日首页

使用 Python 脚本实现图片相似度匹配

2025年5月9日 00:00

随着相机像素越来越大,图片体积也变大了。在图片处理中,较大的文件体积会影响性能,因此杜老师会先生成缩略图,筛选完成后再通过 Python 脚本实现图片相似度匹配。这里是一个简单的示例,供需要的小伙伴们参考。

脚本说明

以下是个基于 Python 的脚本,使用 PIL 以及 imagehash 库来实现。

遍历目录 A 中所有图片。

在目录 B 中查找相似的图片「通过感知哈希算法判断」

如找到匹配项,则将图片复制到目录 C,并以目录 A 图片的名字命名。

脚本代码

在运行脚本前,需安装所需的 Python 库:

1
pip install pillow imagehash

dir_a, dir_bdir_c 替换为实际路径;threshold 控制图像相似度阈值,可以根据需要调整;支持多种常见格式图片文件;使用 imagehash.phash 进行感知哈希的比较,适合用于识别视觉上接近的图片:

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
import os
import shutil
from PIL import Image
import imagehash

# 定义目录路径
dir_a = 'path/to/dirA'
dir_b = 'path/to/dirB'
dir_c = 'path/to/dirC'

# 设置相似度阈值(越小越严格)
threshold = 5

# 获取图片的感知哈希值
def get_image_hash(filepath):
try:
return imagehash.phash(Image.open(filepath))
except Exception as e:
print(f"无法处理文件 {filepath}: {e}")
return None

# 判断两个哈希值是否相似
def is_similar(hash1, hash2):
return hash1 - hash2 <= threshold

# 确保目标目录存在
os.makedirs(dir_c, exist_ok=True)

# 遍历目录 A
for filename in os.listdir(dir_a):
file_a_path = os.path.join(dir_a, filename)

# 检查是否为图片
if not filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
continue

hash_a = get_image_hash(file_a_path)
if hash_a is None:
continue

# 遍历目录 B 寻找相似图片
for b_filename in os.listdir(dir_b):
file_b_path = os.path.join(dir_b, b_filename)

# 检查是否为图片
if not b_filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
continue

hash_b = get_image_hash(file_b_path)
if hash_b is None:
continue

if is_similar(hash_a, hash_b):
# 构建目标路径
file_c_path = os.path.join(dir_c, filename)
# 复制并重命名文件
shutil.copy(file_b_path, file_c_path)
print(f"已找到匹配: {filename} -> {b_filename}, 已复制到 {file_c_path}")
昨天以前首页

使用 Python 脚本下载指定网页的图片文件

2025年3月31日 00:00

有小伙伴反馈说侧边栏随机图出现了重复,有些审美疲劳,要求杜老师再更新一些图片,正好聊天广场有小伙伴分享了一个美图的网址。本文分享如何使用 Python 脚本下载指定网页的图片文件,需要的小伙伴可以参考文中代码。

代码需求

使用 Python 的语言编写一个脚本,下载指定网址中包含的多种格式图片文件,如 JPG 和 PNG 格式图片。

将图片保存至指定的目录中,可以指定绝对路径,或者相对路径。

并用随机数重命名,防止同名图片触发覆盖事件。

尽可能使用 Python 的标准库,尽量避免使用第三方库。

变更解释

  1. 导入必要的库:包括 os/requests/re 以及 random

  2. 定义函数:download_images 函数可用于下载图片;

  3. 获取图片链接:使用正则表达式从网页内容中提取图片 URL;

  4. 下载保存图片:使用 requests 库下载图片,并且使用 random 库生成随机数作为文件名;

  5. 指定目录:确保保存目录存在,如果不存在则创建;

  6. 获取内容:使用 requests 库获取网页内容。

功能代码

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
import os
import requests
import re
import random

def download_images(url, save_folder):
# 确保保存目录存在
if not os.path.exists(save_folder):
os.makedirs(save_folder)

# 发送HTTP请求获取网页内容
response = requests.get(url)
if response.status_code != 200:
print(f"Failed to retrieve the webpage. Status code: {response.status_code}")
return

# 使用正则表达式查找所有的图片URL
image_urls = re.findall(r'<img[^>]+src=["\'](.*?)["\']', response.text)

for img_url in image_urls:
# 处理相对路径的URL
if not img_url.startswith(('http://', 'https://')):
img_url = os.path.join(url, img_url)

# 下载图片
img_response = requests.get(img_url)
if img_response.status_code == 200:
# 生成随机文件名
random_filename = f"{random.randint(10000, 99999)}.jpg"
save_path = os.path.join(save_folder, random_filename)

# 保存图片
with open(save_path, 'wb') as f:
f.write(img_response.content)
print(f"Downloaded and saved {img_url} as {save_path}")
else:
print(f"Failed to download {img_url}. Status code: {img_response.status_code}")

# 读取网址列表文件
def read_urls_from_file(file_path):
with open(file_path, 'r') as file:
urls = file.readlines()
return [url.strip() for url in urls]

# 示例调用
if __name__ == "__main__":
urls_file = 'f:\\代码\\urls.txt' # 包含网址的文件路径
save_folder = 'f:\\代码\\images' # 保存图片的目录路径

urls = read_urls_from_file(urls_file)
for url in urls:
download_images(url, save_folder)

注意:本示例代码仅适用于 Python 3.x 版本,运行于 Windows 系统。如使用 Linux 系统,可能需要进行相应修改。

使用说明

将上述的代码保存为 download_images.py 文件。

在运行脚本时,传入目标网页的 URL 和保存图片的目录路径。

脚本会自动下载网页中所有图片,并且以随机数命名保存到指定目录中。

打开的网址保存在一个文件,每行一个网址。

使用 Python 脚本验证指定目录的图片文件

2025年2月26日 00:00

有小伙伴说杜老师说侧边栏随机图片素材太少,翻来覆去只有那么几张。为了充实随机图片,杜老师采集了一个图片网站。奈何能力有限,某些图片采集失败,保存为空文件,所以需要编写一个脚本,来验证图片是否为正常。

代码需求

使用 Python 语言编写一个小项目,需要遍历指定目录下所有子目录「子目录名称为中文」

验证子目录下的图片文件是否能正常打开,如果能则跳过,如果无法正常打开则返回其路径。

变更解释

  1. 使用 os 模块来遍历目录和子目录;

  2. 使用 PIL 库来验证图片文件是否能正常打开。

注意事项

请确保安装了 Pillow 库,可以使用下面的命令来安装:

1
pip install pillow

root_directory 变量设置为需要遍历的目录路径:

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
import os
from PIL import Image

def validate_images_in_directory(root_dir):
invalid_image_paths = []

for dirpath, dirnames, filenames in os.walk(root_dir):
for filename in filenames:
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
file_path = os.path.join(dirpath, filename)
try:
with Image.open(file_path) as img:
img.verify() # 尝试验证图片文件
except (IOError, SyntaxError) as e:
invalid_image_paths.append(file_path)

return invalid_image_paths

if __name__ == "__main__":
root_directory = '指定目录路径' # 替换为实际的目录路径
invalid_images = validate_images_in_directory(root_directory)

if invalid_images:
print("无法打开的图片文件路径:")
for path in invalid_images:
print(path)
else:
print("所有图片文件都能正常打开。")

执行代码

1
python3 images.py

注意:将上面的代码保存为 images.py 文件,然后在命令行中执行上面的命令。

GitHub 库自动同步脚本分享

2024年4月15日 00:00

杜老师复刻了 70 多个库,每次源库更新后都需要一一手动同步,太过麻烦。今天分享一个自动同步脚本,有需要的小伙伴可以试一下。注意如对本地库有修改,建议使用 PR 来同步,避免代码覆盖。

代码同步

不太清楚小伙伴们同步代码方式,有人习惯用 PR,有人喜欢用下图同步的方式。不管哪种方式,都需要手动操作的。如有仓库过多,每个都要同步一遍,想想是多大工作量。杜老师分享了 GitHub 库自动同步脚本,供有需要的小伙伴参考:

脚本分享

进入要同步的库中,切换至 Actions,点击 New workflow 项:

打开新页面后,点击篮字的 set up a workflow yourself:

设置文件名 sync.yml「可自定义,不与其它脚本同名即可」

将下面的脚本填到输入框中,点击右上方 Commit changes 即可:

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
name: Upstream Sync

permissions:
contents: write

on:
schedule:
- cron: "0 0 * * *" # every day
workflow_dispatch:

jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}

steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v4

# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: arnidan/nsfw-api
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set

# Set test_mode true to run tests instead of the true action!!
test_mode: false

- name: Sync check
if: failure()
run: |
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,您需要手动 Sync Fork 一次。"
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
exit 1

9 个实用的 Shell 脚本

2024年2月12日 00:00

好久没更新了,实在不知道写点什么好,就在网上找了一些资源。正文是杜老师整理的 9 个实用 Shell 脚本,供有需要的小伙伴参考。需要注意的是,这些脚本为杜老师收集,并没有测试过,小伙伴们使用之前要先测试。

写在最前

常来的小伙伴应该发现杜老师说近两周没有更新了,过年期间确实有太多的事情需要处理,各种亲戚走动等等。目前已经处理差不多了,工作上的节奏也已慢慢稳定,近期开始补上之前拖更文章。开头也祝愿来访的小伙伴们龙年大吉,博客访问蒸蒸日上,身体和服务器健健康康,心想和收入皆遂意!

DoS 攻击防范自动屏蔽攻击 IP

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
DATE=$(date +%d/%b/%Y:%H:%M)
LOG_FILE=/usr/local/nginx/logs/demo2.access.log
ABNORMAL_IP=$(tail -n5000 $LOG_FILE | grep $DATE | awk '{a[$1]++}END{for(i in a)if(a[i]>10)print i}')
for IP in $ABNORMAL_IP; do
if [ $(iptables -vnL | grep -c "$IP") -eq 0 ]; then
iptables -I INPUT -s $IP -j DROP
echo "$(date +'%F_%T') $IP" >>/tmp/drop_ip.log
fi
done

Linux 系统发送告警邮件的脚本

1
2
3
4
5
# yum install mailx
# vi /etc/mail.rc
set from=baojingtongzhi@163.com smtp=smtp.163.com
set smtp-auth-user=baojingtongzhi@163.com smtp-auth-password=123456
set smtp-auth=login

MySQL 数据库的单循环备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
DATE=$(date +%F_%H-%M-%S)
HOST=localhost
USER=backup
PASS=123.com
BACKUP_DIR=/data/db_backup
DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "show databases;" 2>/dev/null | egrep -v "Database|information_schema|mysql|performance_schema|sys")

for DB in $DB_LIST; do
BACKUP_NAME=$BACKUP_DIR/${DB}_${DATE}.sql
if ! mysqldump -h$HOST -u$USER -p$PASS -B $DB >$BACKUP_NAME 2>/dev/null; then
echo "$BACKUP_NAME 备份失败!"
fi
done

MySQL 数据库的多循环备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
DATE=$(date +%F_%H-%M-%S)
HOST=localhost
USER=backup
PASS=123.com
BACKUP_DIR=/data/db_backup
DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "show databases;" 2>/dev/null | egrep -v "Database|information_schema|mysql|performance_schema|sys")

for DB in $DB_LIST; do
BACKUP_DB_DIR=$BACKUP_DIR/${DB}_${DATE}
[ ! -d $BACKUP_DB_DIR ] && mkdir -p $BACKUP_DB_DIR &>/dev/null
TABLE_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "use $DB;show tables;" 2>/dev/null)
for TABLE in $TABLE_LIST; do
BACKUP_NAME=$BACKUP_DB_DIR/${TABLE}.sql
if ! mysqldump -h$HOST -u$USER -p$PASS $DB $TABLE >$BACKUP_NAME 2>/dev/null; then
echo "$BACKUP_NAME 备份失败!"
fi
done
done

Nginx 的访问日志按天切割

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
LOG_DIR=/usr/local/nginx/logs
YESTERDAY_TIME=$(date -d "yesterday" +%F)
LOG_MONTH_DIR=$LOG_DIR/$(date +"%Y-%m")
LOG_FILE_LIST="default.access.log"

for LOG_FILE in $LOG_FILE_LIST; do
[ ! -d $LOG_MONTH_DIR ] && mkdir -p $LOG_MONTH_DIR
mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME}
done

kill -USR1 $(cat /var/run/nginx.pid)

Nginx 访问日志的分析脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
#日志格式: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"
LOG_FILE=$1
echo "统计访问最多的 10 个 IP"
awk '{a[$1]++}END{print "UV:",length(a);for(v in a)print v,a[v]}' $LOG_FILE | sort -k2 -nr | head -10
echo "----------------------"

echo "统计时间段访问最多 IP"
awk '$4>="[01/Dec/2018:13:20:25" && $4<="[27/Nov/2018:16:20:49"{a[$1]++}END{for(v in a)print v,a[v]}' $LOG_FILE | sort -k2 -nr | head -10
echo "----------------------"

echo "统计访问最多 10 个页面"
awk '{a[$7]++}END{print "PV:",length(a);for(v in a){if(a[v]>10)print v,a[v]}}' $LOG_FILE | sort -k2 -nr
echo "----------------------"

echo "统计访问页面状态码的数量"
awk '{a[$7" "$9]++}END{for(v in a){if(a[v]>5)print v,a[v]}}'

查看网卡实时流量脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
NIC=$1
echo -e " In ------ Out"
while true; do
OLD_IN=$(awk '$0~"'$NIC'"{print $2}' /proc/net/dev)
OLD_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)
sleep 1
NEW_IN=$(awk '$0~"'$NIC'"{print $2}' /proc/net/dev)
NEW_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)
IN=$(printf "%.1f%s" "$((($NEW_IN - $OLD_IN) / 1024))" "KB/s")
OUT=$(printf "%.1f%s" "$((($NEW_OUT - $OLD_OUT) / 1024))" "KB/s")
echo "$IN $OUT"
sleep 1
done

服务器系统配置初始化脚本

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
#/bin/bash
#设置时区同步时间
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
if ! crontab -l | grep ntpdate &>/dev/null; then
(
echo "* 1 * * * ntpdate time.windows.com >/dev/null 2>&1"
crontab -l
) | crontab
fi

#禁用 SELinux 防火墙
sed -i '/SELINUX/{s/permissive/disabled/}' /etc/selinux/config

#关闭各版本防火墙
if egrep "7.[0-9]" /etc/redhat-release &>/dev/null; then
systemctl stop firewalld
systemctl disable firewalld
elif egrep "6.[0-9]" /etc/redhat-release &>/dev/null; then
service iptables stop
chkconfig iptables off
fi

#历史命令显示操作时间
if ! grep HISTTIMEFORMAT /etc/bashrc; then
echo 'export HISTTIMEFORMAT="%F %T `whoami` "' >>/etc/bashrc
fi

#SSH 的超时时间
if ! grep "TMOUT=600" /etc/profile &>/dev/null; then
echo "export TMOUT=600" >>/etc/profile
fi

#禁止 root 的远程登录
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

#禁止定时任务发送邮件
sed -i 's/^MAILTO=root/MAILTO=""/' /etc/crontab

#设置最大打开的文件数
if ! grep "* soft nofile 65535" /etc/security/limits.conf &>/dev/null; then
cat >>/etc/security/limits.conf <<EOF
* soft nofile 65535
* hard nofile 65535
EOF
fi

#系统内核优化
cat >>/etc/sysctl.conf <<EOF
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_tw_buckets = 20480
net.ipv4.tcp_max_syn_backlog = 20480
net.core.netdev_max_backlog = 262144
net.ipv4.tcp_fin_timeout = 20
EOF

#减少 Swap 的使用
echo "0" >/proc/sys/vm/swappiness

#安装系统性能分析工具
yum install gcc make autoconf vim sysstat net-tools iostat if

监控 100 台服务器磁盘利用率脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
HOST_INFO=host.info
for IP in $(awk '/^[^#]/{print $1}' $HOST_INFO); do
USER=$(awk -v ip=$IP 'ip==$1{print $2}' $HOST_INFO)
PORT=$(awk -v ip=$IP 'ip==$1{print $3}' $HOST_INFO)
TMP_FILE=/tmp/disk.tmp
ssh -p $PORT $USER@$IP 'df -h' >$TMP_FILE
USE_RATE_LIST=$(awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' $TMP_FILE)
for USE_RATE in $USE_RATE_LIST; do
PART_NAME=${USE_RATE%=*}
USE_RATE=${USE_RATE#*=}
if [ $USE_RATE -ge 80 ]; then
echo "Warning: $PART_NAME Partition usage $USE_RATE%!"
fi
done
done

MySQL 数据库备份脚本参考分享

2024年1月25日 00:00

在之前的文件中杜老师推荐了两款云主机面板,有小伙伴留言说因服务器配置比较低,未使用云主机面板,一直为数据库备份问题困扰。杜老师之前工作的环境也是没有面板,编写过相关的备份脚本,在此文中分享给需要的小伙伴们~

设置环境变量

1
2
3
4
5
6
#!/bin/bash
# Setup environment variables
export MYSQL_HOST=localhost
export MYSQL_PORT=3306
export MYSQL_USER=root
export MYSQL_PASSWORD=your_password

注意:需要设置环境变量,以便脚本可以找到 MySQL 数据库。在脚本中,添加以上内容。

创建备份目录

1
2
3
4
5
6
# Create backup directory
BACKUP_DIR=/path/to/backup/directory
# Check if backup directory exists
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
fi

注意:创建一个目录,以存储备份的文件。在脚本中,添加以上内容。

备份 MySQL 数据库

1
2
3
4
# Backup database
DATABASE_NAME=your_database_name
BACKUP_FILE="$BACKUP_DIR/$DATABASE_NAME-$(date +"%Y-%m-%d_%H-%M-%S").sql"
mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --host=$MYSQL_HOST --port=$MYSQL_PORT $DATABASE_NAME > "$BACKUP_FILE"

注意:使用 mysqldump 命令备份数据库。在脚本中,添加以上内容。

检查备份是否成功

1
2
3
4
5
6
7
# Check if backup file exists
if [ -f "$BACKUP_FILE" ]; then
echo "Backup completed successfully!"
else
echo "Backup failed!"
exit 1
fi

注意:检查备份文件是否创建。在脚本中,添加以上内容。

压缩备份文件

1
2
3
4
5
# Compress backup file
COMPRESSED_BACKUP_FILE="$BACKUP_FILE".gz
gzip "$BACKUP_FILE"
# Remove original backup file
rm "$BACKUP_FILE"

注意:为了节省空间,可使用 gzip 命令压缩备份文件。在脚本中,添加以上内容。

删除 MySQL 旧备份

1
2
3
4
5
# Remove old backups
OLD_BACKUPS=$(ls -t "$BACKUP_DIR" | tail -n +7)
if [ ! -z "$OLD_BACKUPS" ]; then
rm "$BACKUP_DIR/$OLD_BACKUPS"
fi

注意:删除旧备份文件以节省空间。在脚本中,添加以上内容。

设置定时任务

最后,可使用 crontab 命令设置定时任务,以便脚本定期运行。在终端中输入以下命令:

1
crontab -e

在打开的编辑器中,添加以下行的内容,比如以每天凌晨的一点运行脚本。保存并退出编辑器:

1
0 1 * * * /path/to/your/backup_script.sh

还有哪些开源工具

除上面脚本外,还有一些开源「有商业版」工具也可实现 MySQL 数据库备份,供需要的小伙伴们参考选择:

名称描述
mysqldumpmysqldump 是 MySQL 官方提供的备份工具,可将 MySQL 数据库的数据和结构导出为 SQL 文件。
XtraBackupXtraBackup 是一个 Percona 提供的备份工具,可在线备份 MySQL 数据,提供增量备份、压缩功能。
Zmanda Recovery ManagerZmanda Recovery Manager 是一款商业的备份和恢复解决方案,可支持 MySQL 和其它数据库。提供完整备份、增量备份、多种存储介质、自动化备份和恢复等功能。
mydumpermydumper 是一个 MySQL 官方派生的高性能 MySQL 备份工具,支持并行备份,可更快地备份大型 MySQL 数据库。

如何删除 GitHub 的提交历史记录

2022年9月17日 00:00

有时候不经意把一些敏感的信息写到了代码里,并提交到 GitHub 上,代码公开时被人发现是很危险的事情,这时候就需要将之前的提交记录进行删除。

需求背景

细心的小伙伴会发现本博最后活动时间永远在 24 小时内,因为杜老师经常会调整博客,包括配置、内容等等。频繁更新消耗了大量 GitHub Actions 部署配额,后经香猪提示,将库公开则不会再消耗部署配额:

公开库的第一时间,就受到了凉心云的警告信息,其在之前的提交中扫描到高权限密钥。为了避免数据泄露,杜老师需要删除 GitHub 提交历史记录:

操作指令

1
2
3
4
5
6
git checkout --orphan master # 在非新存储库上以类似 git init 的状态创建分支
git add -A # 提交所有文件到数据暂存区
git commit -m a # 提交修改
git branch -D main # 删除分支
git branch -m main # 将当前分支重命名
git push -f origin main # 强制提交当前分支

注意:数据宝贵,删除前需做好备份!

Go 语言实现 2048 游戏

2021年5月13日 00:00

相信大家都玩过 2048 这个游戏,这次我们将使用 Go 语言及调用相关包来完成一个简易版的 2048 游戏,快来一同尝试下吧!

执行代码

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
package main

import (
"fmt"
"github.com/shiyanlou/termbox-go"
"math/rand"
"time"
)

var Score int
var step int

func coverPrintStr(x, y int, str string, fg, bg termbox.Attribute) error {

xx := x
for n, c := range str {
if c == '\n' {
y++
xx = x - n - 1
}
termbox.SetCell(xx+n, y, c, fg, bg)
}
termbox.Flush()
return nil
}

type Status uint

const (
Win Status = iota
Lose
Add
Max = 2048
)

type G2048 [4][4]int

func (t *G2048) checkWinOrAdd() Status {
for _, x := range t {
for _, y := range x {
if y >= Max {
return Win
}
}
}
i := rand.Intn(len(t))
j := rand.Intn(len(t))
for x := 0; x < len(t); x++ {
for y := 0; y < len(t); y++ {
if t[i%len(t)][j%len(t)] == 0 {
t[i%len(t)][j%len(t)] = 2 << (rand.Uint32() % 2)
return Add
}
j++
}
i++
}

return Lose
}

func (t G2048) initialize(ox, oy int) error {
fg := termbox.ColorYellow
bg := termbox.ColorBlack
termbox.Clear(fg, bg)
str := " SCORE: " + fmt.Sprint(Score)
for n, c := range str {
termbox.SetCell(ox+n, oy-1, c, fg, bg)
}
str = "ESC:exit " + "Enter:replay"
for n, c := range str {
termbox.SetCell(ox+n, oy-2, c, fg, bg)
}
str = " PLAY with ARROW KEY"
for n, c := range str {
termbox.SetCell(ox+n, oy-3, c, fg, bg)
}
fg = termbox.ColorBlack
bg = termbox.ColorGreen
for i := 0; i <= len(t); i++ {
for x := 0; x < 5*len(t); x++ {
termbox.SetCell(ox+x, oy+i*2, '-', fg, bg)
}
for x := 0; x <= 2*len(t); x++ {
if x%2 == 0 {
termbox.SetCell(ox+i*5, oy+x, '+', fg, bg)
} else {
termbox.SetCell(ox+i*5, oy+x, '|', fg, bg)
}
}
}
fg = termbox.ColorYellow
bg = termbox.ColorBlack
for i := range t {
for j := range t[i] {
if t[i][j] > 0 {
str := fmt.Sprint(t[i][j])
for n, char := range str {
termbox.SetCell(ox+j*5+1+n, oy+i*2+1, char, fg, bg)
}
}
}
}
return termbox.Flush()
}

func (t *G2048) mirrorV() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(t)-i-1][j] = num
}
}
*t = *tn
}

func (t *G2048) right90() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[j][len(t)-i-1] = num
}
}
*t = *tn
}

func (t *G2048) left90() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-j-1][i] = num
}
}
*t = *tn
}

func (t *G2048) right180() {
tn := new(G2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-i-1][len(line)-j-1] = num
}
}
*t = *tn
}

func (t *G2048) mergeUp() bool {
tl := len(t)
changed := false
notfull := false
for i := 0; i < tl; i++ {

np := tl
n := 0

for x := 0; x < np; x++ {
if t[x][i] != 0 {
t[n][i] = t[x][i]
if n != x {
changed = true
}
n++
}
}
if n < tl {
notfull = true
}
np = n
for x := 0; x < np-1; x++ {
if t[x][i] == t[x+1][i] {
t[x][i] *= 2
t[x+1][i] = 0
Score += t[x][i] * step
x++
changed = true
}
}
n = 0
for x := 0; x < np; x++ {
if t[x][i] != 0 {
t[n][i] = t[x][i]
n++
}
}
for x := n; x < tl; x++ {
t[x][i] = 0
}
}
return changed || !notfull
}

func (t *G2048) mergeDwon() bool {
//t.mirrorV()
t.right180()
changed := t.mergeUp()
//t.mirrorV()
t.right180()
return changed
}

func (t *G2048) mergeLeft() bool {
t.right90()
changed := t.mergeUp()
t.left90()
return changed
}

func (t *G2048) mergeRight() bool {
t.left90()
changed := t.mergeUp()
t.right90()
return changed
}

func (t *G2048) mrgeAndReturnKey() termbox.Key {
var changed bool
Lable:
changed = false
//ev := termbox.PollEvent()
event_queue := make(chan termbox.Event)
go func() {
for {
event_queue <- termbox.PollEvent()
}
}()

ev := <-event_queue

switch ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyArrowUp:
changed = t.mergeUp()
case termbox.KeyArrowDown:
changed = t.mergeDwon()
case termbox.KeyArrowLeft:
changed = t.mergeLeft()
case termbox.KeyArrowRight:
changed = t.mergeRight()
case termbox.KeyEsc, termbox.KeyEnter:
changed = true
default:
changed = false
}

if !changed {
goto Lable
}

case termbox.EventResize:
x, y := termbox.Size()
t.initialize(x/2-10, y/2-4)
goto Lable
case termbox.EventError:
panic(ev.Err)
}
step++
return ev.Key
}

func (b *G2048) clear() {
next := new(G2048)
Score = 0
step = 0
*b = *next

}

func (b *G2048) Run() {
err := termbox.Init()
if err != nil {
panic(err)
}
defer termbox.Close()

rand.Seed(time.Now().UnixNano())

A:

b.clear()
for {
st := b.checkWinOrAdd()
x, y := termbox.Size()
b.initialize(x/2-10, y/2-4)
switch st {
case Win:
str := "Win!!"
strl := len(str)
coverPrintStr(x/2-strl/2, y/2, str, termbox.ColorMagenta, termbox.ColorYellow)
case Lose:
str := "Lose!!"
strl := len(str)
coverPrintStr(x/2-strl/2, y/2, str, termbox.ColorBlack, termbox.ColorRed)
case Add:
default:
fmt.Print("Err")
}
key := b.mrgeAndReturnKey()
if key == termbox.KeyEsc {
return
}
if key == termbox.KeyEnter {
goto A
}
}
}

func main() {
var game G2048
game.Run()
}

注意:创建源文件 2048.go,输入以上内容。

执行效果

运行代码 go run 2048.go 可启动游戏:

Go 语言实现 2048 游戏「中篇」

2021年5月10日 00:00

矩阵旋转操作是为了将其它三个方向的移动都转换为向上的移动操作。向下、向左、向右转换为向上操作时,数组需要进行翻转操作参考正文代码。

执行代码

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
82
83
84
85
86
87
package main

import "fmt"

type g2048 [4][4]int

func (t *g2048) MirrorV() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(t)-i-1][j] = num
}
}
*t = *tn
}

func (t *g2048) Right90() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[j][len(t)-i-1] = num
}
}
*t = *tn
}

func (t *g2048) Left90() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-j-1][i] = num
}
}
*t = *tn
}

func (g *g2048) R90() {
tn := new(g2048)
for x, line := range g {
for y, _ := range line {
tn[x][y] = g[len(line)-1-y][x]
}
}
*g = *tn

}

func (t *g2048) Right180() {
tn := new(g2048)
for i, line := range t {
for j, num := range line {
tn[len(line)-i-1][len(line)-j-1] = num
}
}
*t = *tn
}

func (t *g2048) Print() {
for _, line := range t {
for _, number := range line {
fmt.Printf("%2d ", number)
}
fmt.Println()
}
fmt.Println()
tn := g2048{{1, 2, 3, 4}, {5, 8}, {9, 10, 11}, {13, 14, 16}}
*t = tn

}

func main() {
fmt.Println("origin")
t := g2048{{1, 2, 3, 4}, {5, 8}, {9, 10, 11}, {13, 14, 16}}
t.Print()
fmt.Println("mirror")
t.MirrorV()
t.Print()
fmt.Println("Left90")
t.Left90()
t.Print()
fmt.Println("Right90")
t.R90()
t.Print()
fmt.Println("Right180")
t.Right180()
t.Print()
}

注意:创建源文件 martix_rorate.go,输入以上代码。

执行结果

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
origin
1 2 3 4
5 8 0 0
9 10 11 0
13 14 16 0

mirror
13 14 16 0
9 10 11 0
5 8 0 0
1 2 3 4

Left90
4 0 0 0
3 0 11 16
2 8 10 14
1 5 9 13

Right90
13 9 5 1
14 10 8 2
16 11 0 3
0 0 0 4

Right180
0 16 14 13
0 11 10 9
0 0 8 5
4 3 2 1

注意:执行 go run martix_rorate.go 后,输出如上。

使用 Termbox 绘制数据流

2021年5月7日 00:00

Termbox 提供一个最小化的 API,允许程序员编写基于文本的用户界面。在 Linux 操作系统有基于终端的实现,基本思想是对所有主要终端和其他类似终端的 API 上的最大的通用功能子集进行抽象,以最小的方式进行。小的 API 意味着它很容易实现、测试、维护、学习。

重要函数

下面我们简单介绍下比较重要的函数:

函数介绍
termbox.Size()获取 Console 的尺寸
termbox.SetCell(x, y, ch, fg, bg)用于设置字符单元属性,其中 x 表示所在行,y 表示所在列,ch 是要设置的字符,fg 和 bg 分布表示前景色和背景色
termbox.Flush()同步后台缓存。Flush 方法一般用于将后台的处理输出到界面中。如重新绘制一个界面
termbox.Init()在使用 Termbox 进行程序开发时候,我们需要先使用 termbox.Init 方法来初始化
termbox.Close()当不再使用 Termbox 任何功能时候,使用 termbox.Close 来关闭对 termbox 引入
termbox.PollEvent()用于等待键盘事件的触发并返回事件,无事件发生时则会无限等待

绘制代码

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
package main

import "github.com/nsf/termbox-go"
import "math/rand"
import "time"

func draw() {
w, h := termbox.Size()
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
termbox.SetCell(x, y, ' ', termbox.ColorDefault,
termbox.Attribute(rand.Int()%8)+1)
}
}
termbox.Flush()
}

func main() {
err := termbox.Init()
if err != nil {
panic(err)
}
defer termbox.Close()

event_queue := make(chan termbox.Event)
go func() {
for {
event_queue <- termbox.PollEvent()
}
}()

draw()
for {
select {
case ev := <-event_queue:
if ev.Type == termbox.EventKey && ev.Key == termbox.KeyEsc {
return
}
default:
draw()
time.Sleep(10 * time.Millisecond)
}
}
}

注意:创建源文件 random_output.go,输入以上代码。

执行代码

1
go run random_output.go

注意:执行以上代码,就可以在终端中看到五彩缤纷的数据流啦,如果想退出程序需按下ESC键。

运行效果

效果如下:

Java 的 SE/EE/ME 区别知道吗

2020年11月2日 00:00

许多零基础 Java 开发者刚参加 Java 培训时并不知道 JavaSE/JavaEE/JavaME 三者之间的区别,那我们究竟该学习 JavaSE 还是 JavaEE,还是 JavaME 呢?笔者将以通俗易懂的方式给大家讲解这三者之间的区别。

三大版本

JavaSE 即 Java 标准版,它是 JavaEE 和 JavaME 的基础,之前也称为 J2SE,用来开发 C/S 架构的软件,通俗来讲,主要用于开发、部署桌面、服务器以及嵌入设备和实时环境中的应用程序。例如,Java 应用程序开发平台 Eclipse。

JavaEE 企业版,之前被称为 J2EE,JavaEE 是在 JavaSE 基础上构建的,用来开发 B/S 架构的软件,主要针对企业应用开发。例如,电子商务网站、ERP 系统等。

JavaME 微型版,也是以 Java 为基础的,之前被称为 J2ME,它是一套运行专门为嵌入式设备设计的 API 接口规范,主要用于开发移动设备软件和嵌入式设备软件,主要针对消费类电子设备的。例如,手机、电视的机顶盒、汽车导航系统等等。

简单来说,JavaSE 是 Java 的基础,主要针对桌面程序开发;JavaEE 是针对企业应用开发;而 JavaME 是主要针对嵌入式设备软件开发。

JavaEE 企业版

多说一些 JavaEE 企业版相关。

JavaEE 在 JavaSE 的基础进行扩展,增加了一些更加便捷的应用框架。如我们现在常用的 Java 开发三大框架 Spring/Struts 和 Hibernate,我们可以应用这些框架轻松写出企业级的应用软件。

JavaEE 也可以说是一个框架也是一种规范,说它是框架是因为它包含了很多我们开发时用到的组件,例如:Servlet/EJB/JSP/JSTL 等。说它是规范是因为我们开发 Web 应用常会用到的一些规范模式,JavaEE 提供很多规范的接口却不实现,将这些接口的具体实现细节转移到厂商的身上,这样各家厂商推出的 JavaEE 产品虽然名称实现不同,但展现给外部使用的却是统一规范的接口。

例如,我们编写的 JSP 代码,由于大量的显示代码和业务逻辑混淆一起,彼此嵌套,不利于程序维护和扩展。当业务需求发生变化的时候,对于程序员和美工是一个很重的负担。为了程序的易维护性和可扩展性,这就需要我们使用 JavaEE 技术来进行项目开发。

如何用 Python 表白

2020年3月19日 00:00

快到情人节了,作为技术宅男,杜老师教大家如何通过 Python 给女神表白。其实 Python 可以做很多事情,通过其强大的库能实现各种效果,今天杜老师准备了一段很简单的代码,感兴趣的小伙伴可以试一下!

表白代码

1
2
3
4
5
import time
words = input('Please input the words you want to say!:')
for item in words.split():
print('\n'.join([''.join([(item[(x-y) % len(item)] if ((x*0.05)**2+(y*0.1)**2-1)**3-(x*0.05)**2*(y*0.1)**3 <= 0 else ' ') for x in range(-30, 30)]) for y in range(12, -12, -1)]))
time.sleep(1.5);

注意:不需要安装任何库;仅支持英文字母的输入;单词间需要加空格;单词越多,持续效果越久!

运行效果

点击播放效果:

asciicast

❌
❌