从 AppleScript 到 Swift Playground:解决导出日历备注的问题

最近在项目中,遇到了一个问题:从 macOS 的日历应用中导出特定事件的备注内容。

使用 AppleScript

先前用的是 AppleScript:《在 Mac 上将当天的日历事项导出为 TXT 文件》,通过脚本直接访问并提取 iCloud 日历。然而, AppleScript 对于访问日历中部分事件的属性,特别是“备注”字段,无法访问,例如:

-- 设置目标日历和事件名称
set targetCalendar to "日常" -- iCloud 中的日常列表
set targetEventTitle to "A1-今日重点工作安排"

-- 获取今天的日期
set currentDate to current date
set todayStart to currentDate - (time of currentDate)
set todayEnd to todayStart + 1 * days

-- 查找日历中的事件
tell application "Calendar"
	set theCalendar to calendar targetCalendar
	set theEvents to (every event of theCalendar whose summary is targetEventTitle and start date is greater than or equal to todayStart and start date is less than todayEnd)
	
	if (count of theEvents) > 0 then
		set theEvent to item 1 of theEvents
		
		-- 初始化存储详细信息的字符串
		set eventDetails to ""
		
		-- 获取已知属性并打印
		try
			set eventDetails to eventDetails & "标题: " & (get summary of theEvent) & return
		on error
			set eventDetails to eventDetails & "标题: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "开始时间: " & (get start date of theEvent) & return
		on error
			set eventDetails to eventDetails & "开始时间: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "结束时间: " & (get end date of theEvent) & return
		on error
			set eventDetails to eventDetails & "结束时间: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "位置: " & (get location of theEvent) & return
		on error
			set eventDetails to eventDetails & "位置: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "备注: " & (get notes of theEvent) & return
		on error
			set eventDetails to eventDetails & "备注: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "是否为全天事件: " & (get allday event of theEvent) & return
		on error
			set eventDetails to eventDetails & "是否为全天事件: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "创建日期: " & (get creation date of theEvent) & return
		on error
			set eventDetails to eventDetails & "创建日期: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "最后修改日期: " & (get modification date of theEvent) & return
		on error
			set eventDetails to eventDetails & "最后修改日期: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "参与者: " & (get attendees of theEvent) & return
		on error
			set eventDetails to eventDetails & "参与者: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "事件状态: " & (get status of theEvent) & return
		on error
			set eventDetails to eventDetails & "事件状态: 无法获取" & return
		end try
		
		try
			set eventDetails to eventDetails & "URL: " & (get url of theEvent) & return
		on error
			set eventDetails to eventDetails & "URL: 无法获取" & return
		end try
		
		-- 打印所有事件详细信息到控制台
		log eventDetails
		
	else
		log "未找到今日的相关事件"
	end if
end tell

这个脚本试图通过 get notes of theEvent 获取事件备注,但在实际运行过程中,无论如何修改和调整脚本,依然无法成功获取“备注”这一字段。AppleScript 在处理日历中某些复杂的事件属性时,可能存在一些限制。

AppleScript 进一步探索的失败

我尝试了各种不同的获取方法,包括导出事件的其他属性(如标题、开始和结束时间等)。但对于最关心的“备注”内容,AppleScript 就无法获得。最终,我不得不考虑其他替代方案。

image-20240906下午73228179

转向 Swift Playground

在 AppleScript 行不通的情况下,我决定使用 Swift,在 Swift Playground 里开发和测试都挺方便。Swift 提供了对 EventKit 框架的支持,访问日历事件比较灵活和强大。以下是我使用 Swift Playground 成功提取日历事件的代码:

import EventKit
import Foundation

// 创建事件存储对象
let eventStore = EKEventStore()

// 请求访问权限
eventStore.requestAccess(to: .event) { granted, error in
    if granted {
        // 获取当前日期范围
        let now = Date()
        let calendar = Calendar.current
        let startDate = calendar.startOfDay(for: now)
        let endDate = calendar.date(byAdding: .day, value: 1, to: startDate)!

        // 查找事件
        let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
        let events = eventStore.events(matching: predicate)
        
        // 查找目标事件
        for event in events {
            if event.title == "A1-今日重点工作安排" {
                // 打印事件详细信息
                print("标题: \(event.title ?? "无标题")")
                print("开始时间: \(event.startDate)")
                print("结束时间: \(event.endDate)")
                print("位置: \(event.location ?? "无")")
                print("备注: \(event.notes ?? "无备注")")
                // 处理 URL
                if let url = event.url {
                    print("URL: \(url)")
                } else {
                    print("URL: 无")
                }
            }
        }
    } else {
        print("访问事件存储失败: \(error?.localizedDescription ?? "未知错误")")
    }
}

通过 Swift 中的 EKEventStore,我得以轻松访问事件的所有详细信息,包括标题、开始时间、结束时间、位置以及我们之前一直无法获取的“备注”内容。

输出备注到文件

为了进一步扩展功能,我将获取到的备注信息保存到了桌面的文本文件中,方便后续查看和使用。以下是最终代码:

import EventKit
import PlaygroundSupport

// 允许 Playground 执行异步代码
PlaygroundPage.current.needsIndefiniteExecution = true

// 创建事件存储对象
let eventStore = EKEventStore()

// 请求完全访问权限
eventStore.requestFullAccessToEvents { granted, error in
    if granted {
        // 获取当前日期范围
        let now = Date()
        let calendar = Calendar.current
        let startDate = calendar.startOfDay(for: now)
        let endDate = calendar.date(byAdding: .day, value: 1, to: startDate)!

        // 查找事件
        let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
        let events = eventStore.events(matching: predicate)
        
        // 查找目标事件
        var notesArray: [String] = []
        
        for event in events {
            if event.title == "A1-今日重点工作安排" {
                // 获取备注内容
                let notes = event.notes ?? "无备注"
                // 将备注按行拆分并添加到数组中
                let lines = notes.split(separator: "\n").map { String($0) }
                notesArray.append(contentsOf: lines)
            }
        }

        if !notesArray.isEmpty {
            // 构造标题
            let header = "共\(notesArray.count)个重点+遗留工作:\n"
            
            // 添加序号到每个事项
                        let numberedNotes = notesArray.enumerated().map { index, note in
                            let number = String(format: "%02d", index + 1)
                            return "\(number). \(note)"
                        }.joined(separator: "\n")
                        
                        // 将标题和编号内容组合
                        let combinedNotes = header + numberedNotes
            
            // 将标题和备注内容组合
            // let combinedNotes = header + notesArray.joined(separator: "\n")
            
            // 输出到控制台
            print(combinedNotes)
            
            // 获取当前日期并格式化为 YYYY-MM-DD
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd"
            let dateString = dateFormatter.string(from: now)
                        
            // 设置文件名
            let fileName = "keytask-\(dateString).txt"
            
            // 将内容写入到桌面文件
            let fileManager = FileManager.default
            let desktopURL = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first!
            let fileURL = desktopURL.appendingPathComponent(fileName)
            
            do {
                try combinedNotes.write(to: fileURL, atomically: true, encoding: .utf8)
                print("备注内容已成功写入到文件: \(fileURL.path)")
            } catch {
                print("写入文件失败: \(error.localizedDescription)")
            }
        } else {
            print("未找到目标事件的备注内容")
        }
    } else {
        print("访问事件存储失败: \(error?.localizedDescription ?? "未知错误")")
    }

    // 完成 Playground 执行
    PlaygroundPage.current.finishExecution()
}

image-20240906下午73735483

总结

虽然 AppleScript 在处理简单的日历自动化任务时依然是一个很好的选择,但在涉及更复杂的日历事件属性时,它的局限性也显现了出来。这个案例也证明了在 macOS 上,面对复杂自动化需求时,Swift 提供了更强大的解决方案。


将本地 Windows 文件夹同步到远程服务器

这个脚本使用 PSCP 和 Plink 工具,PSCP 用于文件传输,Plink 可以用来执行远程命令。确保已安装 PuTTY 工具包中的 PSCP 和 Plink 工具,并将其路径添加到系统的环境变量中。

@echo off

:: 定义本地文件夹和远程服务器信息
set LOCAL_DIR=C:\Workspace\online\
set REMOTE_SERVER=user@1.2.3.4
set REMOTE_DIR=/kelutmp
set PASSWORD=password

:: 检查本地文件夹是否存在
if not exist "%LOCAL_DIR%" (
    echo Local directory does not exist: %LOCAL_DIR%
    exit /b
)

:: 检查本地文件夹是否为空
for /F "delims=" %%F in ('dir /b "%LOCAL_DIR%"') do set NON_EMPTY=1
if not defined NON_EMPTY (
    echo Local directory is empty: %LOCAL_DIR%
    exit /b
)

set FILE_COUNT=0
for /r "%LOCAL_DIR%" %%f in (*) do set /a FILE_COUNT+=1

if "%FILE_COUNT%"=="0" (
    echo Local directory is empty: %LOCAL_DIR%
    exit /b
)

:: 使用 pscp 将本地文件夹中的所有文件同步到远程服务器
pscp -r -pw "%PASSWORD%" "%LOCAL_DIR%*" %REMOTE_SERVER%:%REMOTE_DIR%

:: 如果同步成功,清空本地文件夹
if %errorlevel%==0 (
    echo Sync successful, clearing local directory: %LOCAL_DIR%
    rmdir /s /q "%LOCAL_DIR%"
    mkdir "%LOCAL_DIR%"
) else (
    echo Sync failed, local directory not cleared.
)

将远程服务器文件同步到本地 Windows

这个脚本通过 Plink 连接远程服务器,查找以 daily- 开头文件并将其下载到本地,使用 PSCP 执行文件传输,整个过程完全自动化。脚本的主要功能步骤如下:

  1. 定义服务器和文件路径:指定远程服务器地址、路径和本地存储目录。
  2. 查找文件:使用 Plink 在远程服务器上查找符合条件的文件(例如,文件名以 daily- 开头)。
  3. 文件下载:如果找到目标文件,则使用 PSCP 将其下载到本地。
  4. 删除原文件:下载完成后,使用 Plink 删除远程服务器上的文件。

工具介绍

  • Plink:Plink 是 PuTTY 软件包中的命令行 SSH 客户端,支持通过 SSH 远程执行命令。
  • PSCP:PSCP 是 PuTTY 软件包中的命令行工具,用于通过 SCP 协议从远程服务器传输文件到本地。
@echo off

:: 定义远程服务器和路径
set SRC_SERVER=user@1.2.3.4
set SRC_PATH=/home/user

:: 定义密码
set SRC_PASSWORD="password"

:: 定义本地目标目录
set DEST_DIR=C:\Workspace\

:: 创建目标目录(如果不存在)
mkdir "%DEST_DIR%" 2>nul

:: 使用 plink 检查是否有以 daily- 开头的文件,并获取文件路径
plink -ssh -pw "%SRC_PASSWORD%" -batch %SRC_SERVER% "find %SRC_PATH% -type f -name 'daily-*' | head -n 1" > file_check.txt

:: 读取文件路径
set /p FILE_PATH=<file_check.txt
del file_check.txt

:: 如果文件路径为空,则表示没有找到文件
if "%FILE_PATH%"=="" (
    echo  daily- not exist
    exit /b
)

:: 从远程服务器下载该文件到本地目标目录,并删除远端文件
pscp -pw "%SRC_PASSWORD%" %SRC_SERVER%:%FILE_PATH% %DEST_DIR%\
plink -ssh -pw "%SRC_PASSWORD%" -batch %SRC_SERVER% "rm -rf %FILE_PATH%

在 Windows 7 上使用批处理脚本同步两个远程服务器间的文件

本文介绍一个批处理脚本在两个远程服务器之间同步文件,并且在操作过程中添加时间戳以便后续管理和审计。

1. 设置环境与变量

首先,定义远程服务器的地址和文件路径,同时还要指定本地的临时存储目录。以下是相关变量的设置:

:: 定义远程服务器和路径
set SRC_SERVER=user@a.b.c.d
set SRC_PATH=/SRC_PATH
set DEST_SERVER=user2@1.2.3.4
set DEST_PATH=/DEST_PATH

:: 定义密码
set SRC_PASSWORD="password1"
set DEST_PASSWORD="password2"

:: 临时存储路径
set TEMP_DIR=C:\Users\Administrator\Desktop\remote_sync

这里的 SRC_SERVERDEST_SERVER 分别是源服务器和目标服务器的地址,SRC_PATHDEST_PATH 是对应的文件路径。TEMP_DIR 是用来存放中间文件的本地目录。

2. 创建本地临时目录

在执行文件传输操作前,确保本地有一个临时目录用于存储从源服务器下载的文件:

:: 创建本地临时目录
mkdir "%TEMP_DIR%" 2>nul

这里使用了 2>nul 来忽略错误输出,避免目录已存在时的提示。

3. 生成带有时间戳的目录名

通过 for 循环来提取所需的年月日、时分( Windows 7 的 %date%%time% 和其他系统有区别,如果是 Windows 10 需要自己验证一下):

for /f "tokens=1-6 delims=/:. " %%a in ("%date% %time%") do (
    set "timestamp=%%b%%c%%d_%%e%%f"
)

此代码通过分隔符 /, :, ., 和空格提取年月日时分,最终生成格式如 20240901_1333 的时间戳。

4. 文件传输与清理

接下来,将从源服务器下载的文件传输到目标服务器,并在传输成功后删除源服务器上的文件:

:: 从第一个远程服务器下载文件到本地临时目录
pscp -pw "%SRC_PASSWORD%" -r %SRC_SERVER%:%SRC_PATH%/* %TEMP_DIR%	

:: 将本地临时目录的文件上传到第二个远程服务器
pscp -pw "%DEST_PASSWORD%" -r "%TEMP_DIR%\*" %DEST_SERVER%:%DEST_PATH%

:: 如果上传成功,删除源服务器上的文件
if %ERRORLEVEL%==0 (
    plink -ssh -pw "%SRC_PASSWORD%" -batch %SRC_SERVER% "rm -rf %SRC_PATH%/*"
    echo 已成功删除源服务器上的文件
) else (
    echo 文件传输失败,未删除源服务器上的文件
)

其中,pscp 是 PuTTY 提供的命令行工具,用于执行安全的文件传输操作。plink 则是一个用于执行远程命令的工具。%ERRORLEVEL% 检查上一步操作是否成功,如果成功则删除源服务器上的文件。

5. 归档与日志管理

最后,将本地的临时目录重命名为带时间戳的目录,以便于管理和备份:

:: 移动临时文件夹到带时间戳的目录
move "%TEMP_DIR%" "%TEMP_DIR%_%timestamp%"

echo 文件传输完成

这一步确保每次文件传输操作都有对应的本地备份,便于日后查看或还原。

最终代码

这是目前最新的版本,做了些微调:

@echo off
chcp 65001 >nul

:: 定义远程服务器和路径
set SRC_SERVER=user@a.b.c.d
set SRC_PATH=/SRC_PATH
set DEST_SERVER=user2@1.2.3.4
set DEST_PATH=/DEST_PATH

:: 定义密码
set SRC_PASSWORD="password1"
set DEST_PASSWORD="password2"

:: 临时存储路径
set TEMP_DIR=C:\Users\Administrator\Desktop\remote_sync
:: 获取源服务器的文件数量
plink -ssh -pw "%SRC_PASSWORD%" -batch %SRC_SERVER% "find %SRC_PATH% -type f | wc -l" > file_count.txt
set /p FILE_COUNT=<file_count.txt
del file_count.txt

:: 判断文件数量是否大于等于2
if %FILE_COUNT% LSS 2 (
    echo 文件数量不足,不执行文件传输
    exit /b
)

for /f "tokens=1-6 delims=/:. " %%a in ("%date% %time%") do (
    set "timestamp=%%b%%c%%d_%%e%%f"
)

:: 创建本地临时目录
mkdir "%TEMP_DIR%" 2>nul

:: 从第一个远程服务器下载文件到本地临时目录
pscp -pw "%SRC_PASSWORD%" -r %SRC_SERVER%:%SRC_PATH%/* %TEMP_DIR%	

:: 将本地临时目录的文件上传到第二个远程服务器
pscp -pw "%DEST_PASSWORD%" -r "%TEMP_DIR%\*" %DEST_SERVER%:%DEST_PATH%

:: 如果上传成功,删除源服务器上的文件
if %ERRORLEVEL%==0 (
    plink -ssh -pw "%SRC_PASSWORD%" -batch %SRC_SERVER% "rm -rf %SRC_PATH%/*"
    echo 已成功删除源服务器上的文件
) else (
    echo 文件传输失败,未删除源服务器上的文件
)

:: 移动临时文件夹到带时间戳的目录
move "%TEMP_DIR%" "%TEMP_DIR%_%timestamp%"

echo 文件传输完成

将远程服务器文件夹同步到本地 Mac

#!/bin/bash
set -e

# 设置远程机器的地址、用户名和密码
remote_host="user@a.b.c"
remote_dir="/remote_dir/"
local_dir="/local_dir/"

# 将远程机器目录下的所有文件复制到本地目录
scp -r "$remote_host:$remote_dir"* "$local_dir"

if [ $? -eq 0 ]; then
    echo "下载 $remote_dir$local_dir ok了!"
    ssh "$remote_host" "rm -rf ${remote_dir}*"

    if [ $? -eq 0 ]; then
        echo "删除 $remote_dir ok了!"
    else
        echo "删除 $remote_dir 失败了!"
        exit 1
    fi
else
    echo "拷贝到本地失败"
    exit 1
fi

注意事项

  • 安全性:在脚本中包含了远程服务器的登录信息,请确保这个脚本的权限设置是安全的,避免未经授权的用户访问。此外,使用scpssh时,使用SSH密钥认证以增强安全性。

  • 文件路径:确保local_dir目录存在且可写,如果目录不存在,scp操作将会失败。如有必要可以在脚本中加入检查和创建目录的逻辑。

  • 删除操作:在删除远程服务器文件之前,务必确保文件已经成功下载至本地。这里可以考虑在删除之前进行额外的校验,例如校验文件的MD5哈希值,以确保文件完整性。


selenium 指定 chrome 和 chromedriver 位置

使用 webdriver.Chrome

  • 通过 chrome_options.binary_location 参数指定 Chrome 浏览器的路径
  • 通过 Service 的 executable_path 参数指定 chromedriver.exe 的位置

以下是一个 Windows 示例代码:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# 设置 Chrome 浏览器的路径
chrome_options = webdriver.ChromeOptions()
chrome_options.binary_location = r'C:\Path\To\Your\Chrome\Application\chrome.exe'  # 替换为 Chrome 路径

# 指定 chromedriver.exe 的路径
service = Service(executable_path=r'C:\Path\To\Your\chromedriver.exe')  # 替换为 chromedriver 路径

# 启动浏览器
driver = webdriver.Chrome(service=service, options=chrome_options)
driver.get('https://www.baidu.com')

关键点:

  1. chrome_options.binary_location: 设置 Chrome 浏览器可执行文件的路径。
  2. executable_path: 设置 chromedriver.exe 的路径。
  3. Service: 使用 Service 对象指定 chromedriver.exe 的路径。
  4. webdriver.Chrome: 通过 service 参数传入 Service 对象。

在内网 Windows 离线安装 miniconda 和 python

这几天在内网环境配置 python,稍作记录。需要强调的是,我的方案并没有 100% 完成,目前所有操作都是在 conda 默认的 base 环境中进行,没有完成 conda 安装特定 python 版本这个场景,急着干手头的活,先这样了。

安装 Minicoda

和 Anaconda 的关系

Miniconda 和 Anaconda 都是由Anaconda, Inc. 开发和维护的。

Anaconda 是 一个功能全面的Python发行版本,包含了Python解释器、Conda包管理器以及大量预安装的科学计算、数据分析、机器学习等领域的第三方库(如NumPy、Pandas、SciPy、Jupyter等)。

Miniconda 是 Anaconda 的一个精简版本。仅包含 Python解释器、Conda包管理器和基本的包管理工具,没有预装第三方库。Miniconda 使用的是BSD许可,这意味着可以在商业场景中免费使用 Miniconda。有一说一,就算 Anaconda 是开源的,我也会选择用 Miniconda。

下载

下载: https://docs.anaconda.com/miniconda/miniconda-install/

官方命令行使用

image-20240829下午45329326

img

conda info
conda info --env # 列出所有环境
conda env list # 列出所有环境
python --version
pip --version

查看 conda 的路径

where conda

复制 Scripts这个路径,将它添加到PATH中

新增 script 文件夹路径到 PATH

控制面板->系统和安全->系统->高级系统设置->环境变量->系统变量->Path

image-20240830下午40517367

这样就可以任意打开 cmd 或者 powershell 开始使用conda了。这也是为了方便和 vscode 等其他工具配合。

下载 python 库

我以下载appium-python-client为例子:

在外网环境中下载

在一台可以访问互联网的电脑上,使用以下命令来下载 appium-python-client 及其依赖项:

pip download appium-python-client -i https://pypi.tuna.tsinghua.edu.cn/simple

image-20240902下午20248045

将下载好的 .whl.tar.gz 文件通过U盘或其他方式转移到离线环境中。

在离线环境中安装包

在离线环境中使用 pip 安装下载的文件:

pip install --no-index --find-links=/path/to/downloaded/files appium-python-client

/path/to/downloaded/files 替换为文件夹的路径。

检查安装结果

安装完成后,运行以下命令检查是否成功安装:

pip show appium-python-client

如果能看到包的信息,说明安装成功。