跳至主要內容

新功能 Amazon S3 Files 讓 S3 桶可以掛載到 EC2

Amazon S3 Files 是一項可以將 S3 桶直接掛載到 EC2 等計算資源的服務,並且以 NFS 檔案系統的形式運行。資料保持在 S3 中,可以使用一般的檔案操作(lscpcat 等)進行讀寫。

S3 Files 是什麼

S3 Files 是基於 Amazon EFS 建立的共享檔案系統,讓使用者能以檔案系統的形式存取 S3 桶中的數據。

主要特點如下:

項目內容
協定NFS 4.1 / 4.2
支援的計算資源EC2、Lambda、ECS、EKS
同時連接數量最多 25,000 個計算資源
讀取吞吐量最大 TB/秒
IOPS超過 1,000 萬/桶
加密TLS(傳輸中)+ AWS KMS(儲存時)
檔案系統功能POSIX 權限、檔案鎖定、讀取後寫入一致性

運作原理

S3 Files 將被訪問的資料自動載入高效能儲存系統,並以低延遲的方式提供服務。

  • 小檔案(預設小於 128 KB):直接從高效能儲存中讀取
  • 大檔案(1 MB 以上):直接從 S3 流式傳輸
  • 寫入:在高效能儲存中寫入後,自動同步到 S3

高效能儲存中的資料,如果在一定時間內(預設 30 天,可設置為 1 - 365 天)未被訪問,將自動刪除。

前提條件

  • AWS 帳戶
  • EC2 實例(Linux)
  • S3 桶(與 EC2 相同區域)
  • 兩個 IAM 角色
    • 建立檔案系統用:對 S3 桶的讀寫權限
    • EC2 實例用:附加 AmazonS3FilesClientFullAccess 管理策略
  • 安全群組:允許 NFS 的 2049 端口通訊

IAM 角色的建立

S3 Files 需要兩個 IAM 角色。

1. 用於建立檔案系統的角色

使用管理控制台時會自動創建,因此不需要手動創建

此角色用於讓 S3 Files 存取桶。

# 創建角色
aws iam create-role \
--role-name S3Files-FileSystem-Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "s3files.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}'

# 附加 S3 Files 客戶端策略
aws iam attach-role-policy \
--role-name S3Files-FileSystem-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FilesClientFullAccess

在創建檔案系統時用 --role-arn 指定此角色。

2. 用於 EC2 實例的角色

未附加 IAM 角色會導致掛載失敗

在 CloudShell 中創建以下角色。

# 創建角色
aws iam create-role \
--role-name EC2-S3Files-Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}'

# 附加 S3 Files 客戶端策略
aws iam attach-role-policy \
--role-name EC2-S3Files-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FilesClientFullAccess

# 創建並附加實例檔案配置
aws iam create-instance-profile \
--instance-profile-name EC2-S3Files-Profile

aws iam add-role-to-instance-profile \
--instance-profile-name EC2-S3Files-Profile \
--role-name EC2-S3Files-Role

將此角色附加到實例上。

設定步驟

1. 準備 S3 桶

在 S3 控制台中創建通用桶,也可以使用現有的桶。

需要特別注意的是,必須啟用桶的版本控制設定

2. 創建檔案系統

從控制台創建

alt text

  1. 在 S3 控制台中選擇桶
  2. 點擊「檔案系統」標籤 → 「創建檔案系統」

從控制台創建後,所有可用區會自動創建掛載目標和存取點。

alt text

  1. 指定前綴和 VPC,然後點擊「創建檔案系統」。

記錄下輸出的檔案系統 ID(例如:fs-0123456789abcdef0)。

3. 在實例中掛載

在終端中執行以下命令。

# 創建掛載點
sudo mkdir /mnt/s3files

# 挂載
sudo mount -t s3files fs-0123456789abcdef0:/ /mnt/s3files
備註

如果無法掛載,請執行以下命令再試一次:

sudo dnf install -y amazon-efs-utils # Amazon Linux, RHEL
# sudo apt install -y amazon-efs-utils (Ubuntu, Debian)
備註

如果在執行 dnf 命令時通訊出現問題,請設置 S3 端點(網關),並指定與實例相同的可用區。

但要注意,S3 端點的路由表應與實例所在的子網相同。

確認掛載情況:

df -h /mnt/s3files

應顯示類似以下內容:

Filesystem Size Used Avail Use% Mounted on
<s3files-dns> 8.0E 129M 8.0E 1% /mnt/s3files

4. 確認運行

cd /mnt/s3files

# 創建檔案
sudo sh -c 'echo "Hello, s3 Files!" > test.txt'

# 讀取檔案
cat test.txt

# 創建目錄
sudo mkdir test-directory

ls -la

# 複製檔案
sudo cp test.txt test-directory/

cd test-directory/

# 確認檔案列表
ls -la

寫入的檔案會在約 1 分鐘內與 S3 桶同步。可以在 S3 控制台確認對象已創建。

aws s3 ls s3://<bucket-name>/

自動掛載設定

為了在重啟後保持掛載,需要將以下內容添加到 /etc/fstab

# 添加到 /etc/fstab
fs-0123456789abcdef0:/ /mnt/s3files s3files _netdev,nofail 0 0

_netdev 是一個選項,確保在網路連接後再進行掛載,這是必需的。加入 nofail 可以防止掛載失敗時導致實例無法啟動。

收費

S3 Files 的收費如下:

  • 高效能儲存使用量:檔案系統中數據的儲存費用
  • 檔案系統存取費用:對高效能儲存進行讀寫操作的費用
  • S3 請求費用:對於直接從 S3 讀取大於 1 MB 的檔案,僅收取 S3 GET 費用

為按量計費的無需配置模式,根據 AWS 的說法,與傳統 S3 和檔案系統間數據複製相比可減少最多 90% 的成本。

總結

  • 使用 S3 Files 可以將 S3 桶掛載為 NFS 檔案系統在 EC2 上
  • 數據保留在 S3 中,可以使用 lscatcp 等正常的檔案操作
  • 透過高效能儲存提供低延遲,未被訪問的數據會自動退避
  • 通過 /etc/fstab 設定自動掛載,即使重啟後也能維持

參考

使用 mitmproxy 查看 AI 程式碼工具的網路通訊

透過將 mitmproxy 設定為中間人代理,可以即時查看 AI 程式碼工具在背後進行的 API 通訊內容。

運作原理

許多 AI 程式碼工具是以 Node.js 開發的應用程式,透過 HTTPS 與外部 API 進行通訊。將 mitmproxy 作為中間人代理插入,並讓 Node.js 信任 mitmproxy 的 CA 憑證,即可解密加密的通訊內容並即時查看。

安裝

使用 uv 安裝 mitmproxy 最為方便。

uv tool install mitmproxy

代理設定

在啟動工具之前,先設定以下環境變數。

$env:HTTPS_PROXY = "http://127.0.0.1:8080"
$env:HTTP_PROXY = "http://127.0.0.1:8080"
$env:NODE_EXTRA_CA_CERTS = "$env:USERPROFILE\.mitmproxy\mitmproxy-ca-cert.pem"

關於 NODE_EXTRA_CA_CERTS

只設定 HTTPS_PROXYHTTP_PROXY 會導致 Node.js 的 TLS 驗證失敗而無法通訊。mitmproxy 在中繼 HTTPS 時使用自簽憑證,而 Node.js 預設會拒絕該憑證。

NODE_EXTRA_CA_CERTS 中指定 mitmproxy CA 憑證的路徑後,Node.js 便會信任該憑證,通訊也就能順利建立。

產生 CA 憑證

mitmproxy 在首次啟動時會自動產生 CA 憑證,並儲存至 ~\.mitmproxy\。若尚未產生,啟動一次即可。

mitmweb

瀏覽器會自動開啟,在 http://127.0.0.1:8081 顯示代理管理介面。

啟動工具

在另一個已設定環境變數的終端機中啟動工具。

開始操作工具後,mitmweb 畫面上就會出現請求流量。

擷取到的通訊內容

端點

工具會向以下端點發送請求。

POST https://api.example.com/v1/messages

請求標頭

x-service-version: ...
content-type: application/json
x-api-key: sk-...

請求本文

{
"model": "model-name",
"max_tokens": 16000,
"stream": true,
"system": [
{
"type": "text",
"text": "..."
}
],
"messages": [
{
"role": "user",
"content": "..."
}
],
"tools": [
{
"name": "Read",
"description": "...",
"input_schema": {}
}
]
}

設定 stream: true 後,會以 Server-Sent Events (SSE) 格式接收串流回應。

回應

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
...
data: {"type":"message_stop"}

可以了解到的資訊

項目內容
API 端點api.example.com/v1/messages
認證方式API 金鑰 (x-api-key 標頭)
串流方式SSE 格式
工具定義每次請求都包含
系統提示詞數千至數萬 token 規模

系統提示詞中包含 AI 程式碼工具的運作原則、可用工具的說明及注意事項等內容。

總結

  • 關鍵在於透過 NODE_EXTRA_CA_CERTS 讓 Node.js 信任 mitmproxy 的 CA 憑證
  • API 通訊採用 SSE 串流格式
  • 可以查看工具定義和系統提示詞等 AI 程式碼工具的內部結構

在 Amazon Cognito 設定密碼金鑰 (WebAuthn)

在使用 AWS SAM 建立的檔案儲存 API 的使用者認證中,我使用了 Amazon Cognito。最近我新增了透過密碼金鑰 (WebAuthn) 登入的功能,因此總結一下設定內容。

前提條件:使用密碼金鑰所需的 Cognito 設定

要在 Cognito 中使用密碼金鑰,必須具備以下所有條件。

要件本次的架構
UserPool 層級ESSENTIALS 以上
管理式登入v2 (新的登入 UI)
自訂網域login.example.com (用於 Relying Party ID)

Cognito 的密碼金鑰必須從管理式登入 v2 UI 中註冊及使用。在 LITE 層級 (免費) 中無法使用 WebAuthn,因此需要 ESSENTIALS 層級。

認證流程

密碼金鑰註冊流程

第一次登入時使用密碼,然後從帳戶設定中註冊密碼金鑰。

密碼金鑰登入流程

註冊後,可以透過「使用密碼金鑰登入」按鈕直接認證。

設定內容

為了新增密碼金鑰,我在 template.yaml (SAM 模板) 上進行了僅 6 行的變更。

變更前

UserPool:
Type: AWS::Cognito::UserPool
Properties:
# ...
Policies:
PasswordPolicy:
MinimumLength: 8
# ...
MfaConfiguration: "OFF"

變更後

UserPool:
Type: AWS::Cognito::UserPool
Properties:
# ...
Policies:
PasswordPolicy:
MinimumLength: 8
# ...
SignInPolicy:
AllowedFirstAuthFactors:
- PASSWORD
- WEB_AUTHN # ← 新增密碼金鑰
MfaConfiguration: "OFF"
WebAuthnRelyingPartyID: login.example.com # ← 指定 RP ID
WebAuthnUserVerification: required # ← 強制要求生物認證

各參數的意義

SignInPolicy.AllowedFirstAuthFactors

初步認證步驟中可用的認證方式列表。只有 PASSWORD 時僅能使用密碼,若加上 WEB_AUTHN 則能選擇密碼金鑰。

WebAuthnRelyingPartyID

WebAuthn 的 Relying Party ID (RP ID)。密碼金鑰將綁定到此網域生成和儲存,因此必須與 實際提供登入頁面的網域一致

本次直接使用自訂網域 login.example.com。若使用 Cognito 的預設網域 (xxx.auth.ap-northeast-1.amazoncognito.com),則需指定該網域。

WebAuthnUserVerification

使用密碼金鑰時的用戶確認等級。

說明
required強制要求生物認證或 PIN 等的身份確認
preferred儘可能要求身份確認,但可不要求
discouraged省略身份確認 (不使用生物認證等)

為了提高安全性,選擇了 required

管理式登入的 UI

在管理式登入 v2 的界面中,密碼金鑰設定後,登入畫面將自動新增「使用密碼金鑰登入」按鈕。第一次註冊時需先使用密碼登入,然後可在帳戶設定中新增密碼金鑰。

部署

sam build
sam deploy --no-confirm-changeset

由於 samconfig.toml 中已定義堆疊名稱、區域及參數,因此每次部署時不需要指定選項。

總結

啟用 Cognito 密碼金鑰的重點總結如下:

  1. 設定為 ESSENTIALS 層級 (LITE 不支持 WebAuthn)
  2. 使用 管理式登入 v2
  3. 指定自訂網域 (或 Cognito 預設網域) 為 RP ID
  4. SignInPolicy.AllowedFirstAuthFactors 中增加 WEB_AUTHN
  5. 使用 WebAuthnUserVerification: required 強制要求生物認證

僅透過 6 行的變更,即可使用密碼金鑰登入。此時仍保留密碼,逐步向密碼金鑰過渡,這正是 Cognito 的便利之處。

在 PowerShell 中呼叫 Vertex AI Gemini API

介紹如何從 PowerShell 經由 Google Cloud 的 Vertex AI 呼叫 Gemini 模型。涵蓋 OpenAI 相容端點與原生 Gemini 端點兩種方式。

認證方式

gcloud auth(推薦)

不需要 API 金鑰,可直接使用現有的 Google Cloud 認證資訊。

$accessToken = (gcloud auth print-access-token)

API 金鑰

$apiKey = $env:VERTEX_API_KEY

端點

OpenAI 相容端點(gcloud auth 認證)

https://{region}-aiplatform.googleapis.com/v1beta1/projects/{projectId}/locations/{region}/endpoints/openapi/chat/completions

請求與回應格式與 OpenAI API 相同。模型名稱需加上 google/ 前綴(例:google/gemini-2.5-flash-lite)。

原生 Gemini 端點(API 金鑰認證)

https://{region}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{region}/publishers/google/models/{model}:generateContent

若需要串流請使用 :streamGenerateContent

基本呼叫範例

OpenAI 相容(gcloud auth)

$projectId = "your-project-id"
$region = "us-central1"
$model = "google/gemini-2.5-flash-lite"
$accessToken = (gcloud auth print-access-token)

$body = @{
model = $model
messages = @(
@{
role = "user"
content = "東京的人口是多少?"
}
)
} | ConvertTo-Json -Depth 10

$uri = "https://$region-aiplatform.googleapis.com/v1beta1/projects/$projectId/locations/$region/endpoints/openapi/chat/completions"

$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json" `
-Headers @{ Authorization = "Bearer $accessToken" } `
-Body $body

$response.choices[0].message.content

原生 Gemini(API 金鑰)

$projectId = "your-project-id"
$region = "us-central1"
$model = "gemini-2.5-flash-lite"
$apiKey = $env:VERTEX_API_KEY

$body = @{
contents = @(
@{
role = "user"
parts = @(
@{ text = "東京的人口是多少?" }
)
}
)
} | ConvertTo-Json -Depth 10

$uri = "https://$region-aiplatform.googleapis.com/v1/projects/$projectId/locations/$region/publishers/google/models/${model}:generateContent?key=$apiKey"

$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json" `
-Body $body

$response.candidates[0].content.parts[0].text

回應結構

OpenAI 相容

$response.choices[0].message.content # 產生的文字
$response.usage.total_tokens # 總 Token 數
$response.model # 使用的模型

原生 Gemini

$response.candidates[0].content.parts[0].text # 產生的文字
$response.usageMetadata.totalTokenCount # 總 Token 數
$response.modelVersion # 使用的模型版本

串流(streamGenerateContent)會回傳多個 chunk 的陣列,需將文字串接取出。

$fullText = ($response | ForEach-Object {
$_.candidates[0].content.parts[0].text
}) -join ""

加入系統提示

OpenAI 相容

$body = @{
model = $model
messages = @(
@{
role = "system"
content = "您是用日語回答的 AI 助理,請簡潔回覆。"
}
@{
role = "user"
content = "光速是多少?"
}
)
} | ConvertTo-Json -Depth 10

原生 Gemini

$body = @{
system_instruction = @{
parts = @(
@{ text = "您是用日語回答的 AI 助理,請簡潔回覆。" }
)
}
contents = @(
@{
role = "user"
parts = @(@{ text = "光速是多少?" })
}
)
} | ConvertTo-Json -Depth 10

多回合對話

將歷史對話依序放入陣列即可實現多回合對話。

OpenAI 相容

$body = @{
model = $model
messages = @(
@{ role = "user"; content = "你比較喜歡貓還是狗?" }
@{ role = "assistant"; content = "我喜歡貓。" }
@{ role = "user"; content = "為什麼?" }
)
} | ConvertTo-Json -Depth 10

原生 Gemini

助理的角色需指定為 "model"

$body = @{
contents = @(
@{
role = "user"
parts = @(@{ text = "你比較喜歡貓還是狗?" })
}
@{
role = "model"
parts = @(@{ text = "我喜歡貓。" })
}
@{
role = "user"
parts = @(@{ text = "為什麼?" })
}
)
} | ConvertTo-Json -Depth 10

可用模型

模型OpenAI 相容名稱特性
gemini-2.5-flash-litegoogle/gemini-2.5-flash-lite輕量、快速、低成本
gemini-2.5-flashgoogle/gemini-2.5-flash平衡型
gemini-2.5-progoogle/gemini-2.5-pro高精度,適合複雜任務

選擇方式的判斷基準

情況推薦方式
已有 GCP 認證的環境(開發、CI 等)OpenAI 相容 + gcloud auth
只能使用 API 金鑰原生 Gemini
從 OpenAI 移轉中OpenAI 相容(最小化程式碼變更)
需要串流原生 Gemini

注意事項

  • 請勿將 API 金鑰硬寫在腳本中,應從環境變數($env:VERTEX_API_KEY)讀取。
  • gcloud auth 的 token 有效期限約為 1 小時,長時間執行的腳本需要定期重新取得。
  • 每個專案有各自的速率限制與配額,大量發送請求前請事先確認。

什麼是 CORS?從安全性角度為初學者解說

説明 Web 瀏覽器的安全功能 CORS (Cross-Origin Resource Sharing),涵蓋「為什麼需要」和「有什麼風險」,為初學者量身打造的解説。正確理解 CORS 能夠實現安全的 Web 開發。

CORS 出現的背景:同源政策 (Same-Origin Policy)

在 1990 年代初期,JavaScript 被引入瀏覽器時,Web 安全的概念幾乎不存在。當時,惡意網站可以自由存取其他網站的資料,容易導致工作階段劫持 (Session Hijacking) 和資料竊取。

為了解決這個問題,引入了一項稱為同源政策 (Same-Origin Policy) 的限制。這是一個簡單而強大的規則:「從網頁加載的 JavaScript 無法存取該網頁不同源的資料」。

例如,從 https://www.example.com 加載的 JavaScript 無法存取 https://www.bank.com 的資料。這樣,即使使用者登入銀行網站後訪問惡意網站,銀行資訊也不會被盜取。

什麼是源 (Origin)

源 (Origin) 由以下三個要素決定:

  • 協議 (Protocol)http://https://
  • 主機 (Host/網域)example.comapi.example.com
  • 連接埠 (Port)808080

例如,

URL協議主機連接埠
https://www.example.com/pageHTTPSwww.example.com443 (預設)https://www.example.com
https://api.example.com/dataHTTPSapi.example.com443 (預設)https://api.example.com

由於主機不同,它們被視為不同的源

同源政策防止的事項

JavaScript 的 XHR (XMLHttpRequest)Fetch API 對不同源的請求受到限制。

例:evil.com 上的惡意指令碼

fetch('https://bank.example.com/api/transfer', {
method: 'POST',
body: JSON.stringify({ amount: 1000000 })
});

沒有同源政策的話,惡意網站 (evil.com) 的 JavaScript 可以在使用者登入銀行網站的狀態下發送轉帳請求。防止這種情況就是同源政策的目的

為什麼需要 CORS

然而,現代 Web 開發經常涉及多個源的協作。

  • 前端https://www.example.com
  • API 伺服器https://api.example.com
  • CDN / 靜態文件https://cdn.example.com

這些是由同一公司營運的正當通訊。但如果受到同源政策限制,應用程式就無法運作。

這時候 CORS (Cross-Origin Resource Sharing) 就派上用場了。

什麼是 CORS:明確允許存取

CORS 是伺服器明確聲明「允許來自此源的請求」的機制

伺服器只需返回以下回應標頭,瀏覽器就會放寬限制。

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization

除非伺服器表示「允許」,否則瀏覽器不會將請求結果傳回 JavaScript。這樣既能實現跨源存取,又能保持安全性。

CORS 的安全風險:常見的設定錯誤

雖然 CORS 很便利,但設定不當會造成安全漏洞。

錯誤:允許所有源

Access-Control-Allow-Origin: *

這表示「世界上任何人都可以存取此伺服器」。

// https://evil.com 的 JavaScript
fetch('https://api.example.com/user/profile')
.then(r => r.json())
.then(data => {
// 竊取使用者個人資料的處理
console.log(data);
});

對於包含認證憑證(如 Cookie)的請求特別危險,使用者在登入 api.example.com 後訪問 evil.com 時,個人資訊可能被盜取。

fetch('https://api.example.com/user/profile', {
credentials: 'include' // 包含 Cookie
})

包含認證資訊時,不能使用 Access-Control-Allow-Origin: *。必須明確指定特定的源

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

錯誤:直接允許使用者指定的 URL

// 危險的實現範例(伺服器端)
const origin = request.headers.get('Origin');
response.headers.set('Access-Control-Allow-Origin', origin); // 原樣返回!

這樣做會允許來自 https://evil.com 的請求,回應會帶有 Access-Control-Allow-Origin: https://evil.com,容易被濫用。

正確做法是準備白名單,只允許列表中的源

const allowedOrigins = [
'https://www.example.com',
'https://admin.example.com'
];

if (allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
}

錯誤:允許所有標頭

Access-Control-Allow-Headers: *

這表示「接受任何標頭」,允許透過自訂標頭注入惡意資料的攻擊。

只列出必要的標頭

Access-Control-Allow-Headers: Content-Type, Authorization

CORS 預檢請求:瀏覽器的事前確認

對於簡單請求以外的請求(GET、HEAD、POST),瀏覽器會自動發送 OPTIONS 方法的請求來確認「這樣可以嗎?」這被稱為預檢請求 (Preflight Request)。

1. JavaScript 嘗試發送 PUT 請求

2. 瀏覽器自動發送 OPTIONS 預檢請求
OPTIONS /api/resource HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

3. 伺服器回應「OK」
HTTP 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Content-Type

4. 瀏覽器發送實際的 PUT 請求

如果伺服器不支援 OPTIONS 方法,預檢就會失敗,實際請求也不會被發送。

要點:

  • 在白名單中明確指定允許的源
  • 使用 credentials: true 處理包含 Cookie 認證的請求
  • 只允許必要的方法和標頭
  • OPTIONS 預檢請求

常見疑問

Q. 遇到 CORS 錯誤。可以允許所有源來解決嗎?

A: 不可以。可能會暫時有效,但在生產環境中允許 * 是安全風險。需要重新檢查伺服器端設定或重新設計 API。

Q. 想在本地開發時存取不同連接埠的源。可以嗎?

A: 在本地開發環境中只停用 CORS 是可以的。

Q. 從行動應用程式呼叫 API 時,CORS 有影響嗎?

A: CORS 是瀏覽器的安全功能,所以對行動應用程式沒有影響。取而代之,需要使用 API 金鑰或 OAuth 實作認證和授權。

總結

重點說明
CORS 的目的在保持瀏覽器安全性的同時允許跨源存取
危險的設定對需要認證的 API 使用 Access-Control-Allow-Origin: *
正確的設定在白名單中明確指定允許的源
包含 Cookie 時必須指定 Access-Control-Allow-Credentials: true 和具體的源
預檢PUT/DELETE 等複雜請求需要瀏覽器用 OPTIONS 進行事前確認

CORS 不只是「導致錯誤的原因」,而是 Web 安全的重要機制。設定錯誤可能導致安全事故,因此需要謹慎處理。

參考資料

使用 OpenAI API 開發支援多語言的翻譯 CLI 工具 translate-mcp

translate-mcp 是一個使用 OpenAI API 的翻譯工具。它支援 CLI 模式和 MCP 伺服器的兩種使用方式,能夠應對從想要翻譯整個文件到希望與 AI 工具整合的各種場合。

translate-mcp 是什麼

translate-mcp 是一款 專為翻譯而設的工具,使用 OpenAI API。它是用 Python 實現的,具有以下兩種使用方法。

  1. CLI 模式: 可以直接從命令行翻譯文件
  2. MCP 伺服器模式: 充當 Model Context Protocol (MCP) 伺服器,整合到 AI 工具中

特徵

  • 多語言支援: 支援多種語言
  • 簡單易用: 只需一個 API 金鑰便可開始
  • 兩種使用方式: 既可作為 CLI 腳本,也能作為 MCP 伺服器運行
  • 輕量級: 無需依賴外部庫,僅依賴 OpenAI API
  • 錯誤處理: CLI 模式以 stderr 輸出,MCP 模式以 JSON 格式回傳

安裝

前提條件

  • 已安裝 Python
  • 已獲取 OpenAI API 金鑰

安裝 uv

由於 translate-mcp 由 uv 管理,因此需要先安裝 uv

安裝 uv 的方法參見 Installation | uv

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

安裝 translate-mcp

如果使用 uv,可以使用以下命令進行安裝。

uv tool install git+https://github.com/Himeyama/translate-mcp

如果使用 uvx,則無需安裝,直接執行即可。

uvx git+https://github.com/Himeyama/translate-mcp --help

使用方法

CLI 模式

翻譯文件時,可以使用以下命令執行。

translate --input blog/2026-04-02-example.md --from Japanese --to English

結果將輸出至標準輸出,並可透過重定向保存到文件中。

translate \
--input blog/2026-04-02-example.md \
--from Japanese \
--to English \
--output i18n/en/blog/2026-04-02-example.md

參數

  • --mcp: MCP 模式
  • --input: 待翻譯文件的路徑
  • --from: 原始語言(例如: Japanese、English)
  • --to: 目標語言(例如: English、Taiwanese)
  • --output (可選): 儲存翻譯後文本的位置
  • --model (可選): OpenAI 模型(例如: gpt-5-mini)
  • --debug (可選): 調試模式

錯誤處理

若發生錯誤,將輸出至 stderr。

MCP 伺服器模式

啟動為 MCP 伺服器,並可供 Claude Code 或其他 AI 工具使用。

translate --mcp

實例

以下說明如何將部落格文章從日文翻譯成英文和繁體中文(台灣)。

日文版本(原文)

# blog/2026-04-02-example.md 中有日文的文章

生成英文版本

translate \
--input blog/2026-04-02-example.md \
--from Japanese \
--to English > i18n/en/docusaurus-plugin-content-blog/2026-04-02-example.md

生成台灣版本(繁體中文)

translate \
--input blog/2026-04-02-example.md \
--from Japanese \
--to Taiwanese > i18n/zh-TW/docusaurus-plugin-content-blog/2026-04-02-example.md

優缺點

優點

  • 高精度: 使用 OpenAI 的高品質模型(如 GPT-4)
  • 技能支援: 可整合至 ChatGPT 和 Claude 的多種工具
  • 簡單: 設置和使用都非常簡單
  • 可定制: 由於源代碼公開,因此可進行自定義

缺點

  • 需支付 API 費用: 根據翻譯量,OpenAI API 需要收費
  • 必須連網: 需調用 API,因此無法離線使用
  • 速率限制: 受限於 OpenAI API 的速率限制

總結

translate-mcp 是一個 簡單且高品質的翻譯工具,利用 OpenAI API 開發。它在部落格文章的多語言支援、文件翻譯以及與 AI 工具整合等多種場合中均能發揮作用。

特別是在想要使用 Docusaurus 等靜態網站生成器支援多語言時,作為自動化腳本的用途效果顯著。

參考資料

比較 Anthropic API 與 AWS Bedrock 的費用

Claude 若要透過 API 使用,除了直接使用 Anthropic API 外,也能經由 AWS BedrockGoogle Vertex AIMicrosoft Azure (Azure AI Foundry) 使用。基本價格各路徑幾乎相同,但在批次處理與與雲端生態系統整合面會有差異。

單位:USD / 1M 代幣 (MTok)。資料截至 2026 年 3 月。

基本價格(按需)

模型項目Anthropic APIBedrockVertex AIAzure
Claude Opus 4.6輸入$5.00$5.00$5.00$5.00
輸出$25.00$25.00$25.00$25.00
Claude Sonnet 4.6輸入$3.00$3.00$3.00$3.00
輸出$15.00$15.00$15.00$15.00
Claude Haiku 4.5輸入$1.00$1.00$1.00$1.00
輸出$5.00$5.00$5.00$5.00
Claude Sonnet 4.5輸入$3.00$3.00$3.00$3.00
輸出$15.00$15.00$15.00$15.00

基本價格在各路徑相同。

不過在 Vertex AI 若不是使用全球端點而指定區域端點(regional endpoint),標準價格會額外加收 10%。Bedrock 有 Long Context 變體(另有 SKU),但價格相同。Anthropic API 則已將 Long Context 整合在一般模型中。

快取費用

Prompt 快取(Prompt Caching)的費用在各路徑也相同。

模型快取類型Anthropic APIBedrockVertex AIAzure
Claude Opus 4.65 分快取寫入$6.25$6.25$6.25$6.25
1 小時快取寫入$10.00$10.00$10.00$10.00
快取讀取$0.50$0.50$0.50$0.50
Claude Sonnet 4.65 分快取寫入$3.75$3.75$3.75$3.75
1 小時快取寫入$6.00$6.00$6.00$6.00
快取讀取$0.30$0.30$0.30$0.30
Claude Haiku 4.55 分快取寫入$1.25$1.25$1.25$1.25
1 小時快取寫入$2.00$2.00$2.00$2.00
快取讀取$0.10$0.10$0.10$0.10

快取寫入依 TTL 分為 5 分(短期)和 1 小時(長期)兩種。長期快取的寫入成本較高,但如果系統提示(system prompt)很長且會被重複參考,透過節省讀取費用通常是划得來的。

批次處理費用

Bedrock、Vertex AI、Anthropic API 三者的非同步批次 API 可享按需價格的 50% 折扣。Azure 目前未明示。

模型批次輸入批次輸出
Claude Opus 4.6$2.50$12.50
Claude Sonnet 4.6$1.50$7.50
Claude Haiku 4.5$0.50$2.50
Claude Sonnet 4.5$1.50$7.50

若要大量處理資料(例如日誌分析、向量嵌入生成等)並頻繁使用批次處理,無論走哪個路徑都能把成本砍半。

生態系比較

比較重點Anthropic APIBedrockVertex AIAzure
基本價格相同相同相同相同
區域加價+10%(區域端點)
批次處理(50% OFF)未明示
東京區域
IAM / 審計日誌整合AWSGoogle CloudAzure
VPC / PrivateLink
計費整合Anthropic 直接AWSGoogle CloudAzure
新功能推出速度最快會延遲會延遲會延遲

新功能(例如 Extended Thinking)通常會先在 Anthropic API 上推出,向 Vertex AI、Bedrock、Azure 的推展有時會晚幾週。

如何選擇

  • 單純使用/原型開發:Anthropic API,只要一組 API 金鑰即可啟動,新功能也能最先使用。
  • 已整合到 AWS:若需 IAM、CloudWatch、VPC,選 Bedrock。支援東京區域。
  • 已整合到 Google Cloud:Vertex AI 是自然的選擇,但注意區域端點會加收 10%。
  • 已整合到 Azure:可透過 Azure AI Foundry 使用,能整合到 Azure 的計費與管理。
  • 大量使用批次以節省成本:Bedrock、Vertex AI、Anthropic API 都有 50% OFF 的批次 API 可用。

參考

即使是明朝體的文章,加粗常會變成黑體的理由

即使是以明朝體或襯線字體書寫的文章,加粗時常以黑體(無襯線字體)呈現。這是設計上刻意的選擇,主要有可讀性、視認性、與歷史慣習三個理由。

維持可讀性

若單純把明朝體加粗,襯線(裝飾筆畫)與主筆畫的對比會變得過大,字形內部的空白(counter)容易消失。這在 Web 或螢幕上的小尺寸顯示尤其明顯,字形會潰掉而難以辨認。

黑體本身沒有襯線,因此即使提高字重也能保留字形內的空白,不會明顯降低可讀性。

明確傳達強調意圖

直接改變字體家族,可以讓「這裡是強調」的意圖在視覺上被更即刻地傳達。與單純把明朝體變粗相比,切換到黑體更容易被人眼當作「不同資訊」來處理,強調效果更明確。

歷史與文化背景

自日本活字印刷時代起,就有「強調使用不同字體」的慣例。將明朝體做得更粗在活字鑄造上會產生問題,因此會用完全不同的黑體來表示強調。這個慣習被沿用到數位排版與 Web。

在 CSS 中的實作

若在 Web 以明朝體作為基底字體,可以用以下樣式將加粗切換為黑體。

body {
font-family: "Noto Serif JP", serif;
}

strong, b, h1, h2, h3, h4, h5, h6 {
font-family: "Noto Sans JP", sans-serif;
font-weight: 700;
}

這個做法也有實作上的優點。無須載入明朝體的多個字重,只要為本文提供 Regular,以及為強調提供黑體的 Bold,就能減少頁面載入成本。

總結

日文(以及台灣中文排版時常類比的情況)中,加粗時使用黑體的理由可歸納為三點:

  1. 可讀性:黑體提高字重仍能保留字形內部空白,維持可讀性
  2. 視認性:改變字體家族比單純加粗更清楚地傳達強調
  3. 慣習:自活字印刷時代起的「強調用別書體」的做法被沿用

在 Web 上也因為相同理由採用這種組合,是設計與實作上都合理的選擇。

在 Docusaurus 部落格把 PageSpeed Insights 幾乎做滿分的方法 — SEO、效能、可及性

我把這個部落格的行動版 PageSpeed Insights 分數改善到 Performance 99、Accessibility 100、Best Practices 100、SEO 100。從 SEO、效能、可及性 三個面向整理我做過的改動。

改善前的問題

在原生的 Docusaurus 部落格跑 PageSpeed Insights 時,發現了幾個問題。

  • SEO:沒有 meta description、沒有 OGP/Twitter Cards、沒有結構化資料、網站地圖的 priority 未設定
  • 效能:因為 Google Tag Manager (GTM) 同步載入導致未使用的 JS 過大、從外部 CDN 取得 avatar 成為瓶頸
  • 可及性:主要顏色的對比度未達 WCAG AA 標準

SEO 的改善

新增 meta description、OGP、Twitter Cards

docusaurus.config.tsthemeConfig.metadata 中加入站點共通的 meta 資訊。

themeConfig: {
metadata: [
{ name: 'description', content: "ひかりの技術メモ..." },
{ property: 'og:locale', content: 'ja_JP' },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:site', content: '@ptrqr' },
],
}

另外,透過 Swizzle 覆寫 src/theme/Layout/index.tsx,為沒有自訂 description 的頁面(例如部落格列表、標籤頁)設定以語系為單位的備用 description。

新增 robots.txt

加入 static/robots.txt,明確告訴爬蟲網站地圖的位置。

BlogPosting JSON-LD(結構化資料)

透過 Swizzle 覆寫 src/theme/BlogPostItem/index.tsx,在文章頁輸出包含 headlinedatePublisheddateModifiedauthorBlogPosting JSON-LD。

不過後來發現 Docusaurus 內建的 BlogPostPage/StructuredData 已經會輸出相同的資料。把自訂的 JSON-LD 移除,改為在內建元件上加上 keywords 的備援(frontMatter.keywordstags)。結構化資料重複會對 SEO 造成負面影響,整理這點相當重要。

WebSite JSON-LD

docusaurus.config.tsheadTags 加入 WebSite 類型的 JSON-LD,讓 Google 能正確辨識站名。

headTags: [
{
tagName: 'script',
attributes: { type: 'application/ld+json' },
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'ひかりの備忘録',
url: 'https://www.hikari-dev.com/',
}),
},
],

自動為所有文章生成 OGP 圖片

建立 scripts/generate-ogp.js,自動根據標籤產生漸層背景的 OGP 圖片。如此一來,所有文章在社群分享時都會帶有吸睛的圖片。為每篇文章的 frontMatter 設定 image: 欄位,若文章已有專屬圖片則優先使用。

改善網站地圖

使用 createSitemapItems 回呼把首頁 priority 設為 1.0、部落格文章設為 0.8。另外從 URL 含有的日期自動抽出 lastmod

hreflang x-default

src/theme/Root.tsx 注入帶有 hreflang="x-default"<link>,將英文頁面(/en/...)對應到預設(日文) URL,讓搜尋引擎能正確辨識語言變體。

const defaultPath = pathname.replace(/^\/en(?=\/|$)/, '') || '/';
const xDefaultUrl = `${siteConfig.url}${defaultPath}`;

<Head>
<link rel="alternate" hreflang="x-default" href={xDefaultUrl} />
</Head>

效能的改善

GTM 延遲載入

移除 @docusaurus/plugin-google-gtag,改在 src/clientModules/gtag.js 裡於 window.load 事件之後動態注入 GTM 腳本。如此一來,可大幅減少阻塞初始渲染的未使用 JS。

function loadGtag() {
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
document.head.appendChild(script);
}

window.addEventListener('load', loadGtag, { once: true });

在 SPA 的頁面切換時使用 Docusaurus 的 onRouteDidUpdate hook 手動呼叫 window.gtag。另外也使用 requestIdleCallback 在閒置時載入加以優化。

將 avatar 自行託管並轉為 WebP

把 avatar 圖片從 GitHub CDN(avatars.githubusercontent.com)移到自行託管。GitHub CDN 的快取 TTL 很短(5 分鐘),在 PageSpeed Insights 上每次都會被視為外部請求。

把 avatar 轉為 WebP,檔案從 34 KB(PNG)降到 3.5 KB,約減少 90%。

圖片尺寸優化與修正 CLS

  • 在 GitHub avatar URL 加上 ?size=64,把 460 px 縮到 64 px(減少 33 KB)
  • 在導航列 logo 加上 width/height 屬性以修正 CLS(Cumulative Layout Shift)
  • <img> 標籤加入 loading="lazy"

導入 rspack / SWC

安裝 @docusaurus/faster,把 webpack 換成 rspack + SWC + lightningCSS。

future: {
v4: true,
experimental_faster: true,
},

如此一來,建置速度和打包體積都改善了。

停用未使用的插件

停用未使用的 docs 外掛,避免不必要的 JS 被傳到用戶端。

僅在行動裝置載入 Google Fonts

Noto Sans JP 在我的站只在行動版需要。利用 matchMedia 判斷行動裝置,僅在行動裝置動態注入字型樣式表,桌面版因此減少約 130 KB 的未使用 CSS。

可及性的改善

修正對比度

把主要色從 #F15EB4 改為 #C82273,在白底下達到對比度 5.3:1(符合 WCAG AA)。深色模式則使用 #F36AB2(在暗底下為 7.0:1)。

文章日期文字的顏色改用 CSS 變數 --post-date-color 管理,淺色模式為 #595959(7.0:1),深色模式為 #9e9e9e

統一字型

把標題與 <strong> 的字型從 Noto Serif JP 改為 Noto Sans JP,以與內文保持一致性。

結果

類別分數
Performance99
Accessibility100
Best Practices100
SEO100

在行動裝置上幾乎達到滿分。

總結

對我影響最大的三項變更如下:

  1. GTM 的延遲載入:大幅減少未使用的 JS,效能分數大幅提升
  2. OGP 與結構化資料的整理:達成 SEO 100,社群分享時的呈現也改善
  3. 修正對比度:符合 WCAG AA 讓可及性達到 100

Docusaurus 預設就能生成高品質網站,但要在 PageSpeed Insights 上追求幾乎滿分,仍需針對 GTM 的載入策略、meta 資訊與可及性的細節做調整。希望對也在做同類改善的人有所幫助。

使用 pLaTeX 製作日文 PDF 的方法

整理了使用 pLaTeX 製作日文 PDF 文件的步驟。從範本的構成到編譯,以及常見錯誤的處理都會說明。

pLaTeX 是什麼

pLaTeX 是支援日文組版的 LaTeX 實作。使用 platex 指令編譯 .tex 檔,然後用 dvipdfmx 轉成 PDF。

因為包含在 TeX Live 中,如果已安裝 TeX Live 就可以立即使用。TeX Live 的安裝步驟請參考「在 Linux 上安裝 TeX Live 2026」。

選擇文件類別

在日文文件中可使用以下類別。

類別用途
jarticle論文、報告等短篇文件
jbook書籍、有章節的長篇文件
jreport技術報告(含 abstract + chapter 結構)
beamer投影片(簡報)

範本(jarticle)

使用 jarticle 的基本範例如下。

顯示範例程式碼
% pLaTeX 範例文件 — 主要功能概覽
\documentclass[a4paper,12pt]{jarticle}

%% ========== 套件 ==========
\usepackage{amsmath, amssymb} % 數學式強化
\usepackage{graphicx} % 插入圖像
\usepackage{color} % 顏色設定
\usepackage{fancyhdr} % 頁首/頁尾
\usepackage{geometry} % 頁面版面
\usepackage{enumerate} % 列舉自訂
\usepackage{url} % URL 顯示
\usepackage{multicol} % 分欄
\usepackage{booktabs} % 高品質表格線
\usepackage{array} % 表格欄格式擴充
\usepackage{verbatim} % verbatim 環境
\usepackage{ascmac} % 框線環境(screen, itembox 等)
\usepackage{okumacro} % Ruby・圓點等(pLaTeX 常用)
\usepackage{setspace} % 行距調整
\usepackage{listings} % 原始碼清單
\usepackage{xcolor} % 顏色(listings 用)
\usepackage{caption} % 標題樣式

%% ========== 頁面版面 ==========
\geometry{top=25mm, bottom=25mm, left=25mm, right=25mm}
\setlength{\headheight}{17pt}
\addtolength{\topmargin}{-5pt}

%% ========== 頁首/頁尾 ==========
\pagestyle{fancy}
\fancyhf{}
\lhead{pLaTeX 範例文件}
\rhead{\today}
\cfoot{\thepage}
\renewcommand{\headrulewidth}{0.4pt}

%% ========== listings 設定 ==========
\lstset{
basicstyle=\ttfamily\small,
keywordstyle=\color{blue}\bfseries,
commentstyle=\color{green!50!black},
stringstyle=\color{red!70!black},
numbers=left,
numberstyle=\tiny\color{gray},
frame=single,
breaklines=true,
tabsize=4,
}

%% ========== 標題資訊 ==========
\title{\textbf{pLaTeX 主要功能範例}\\[5pt]
\large --- 日文 \LaTeX 的典型用法 ---}
\author{範例太郎\thanks{範例大學 資訊工程系}
\and 範例花子}
\date{\today}

%% ================================================================
\begin{document}
%% ================================================================

\maketitle
\thispagestyle{fancy}

\begin{abstract}
本文檔是示範 pLaTeX 主要功能的範例。
依序說明文件類別與套件的讀入、日文組版特有的功能、
數學式、表格、圖形、交叉參照、腳註、Ruby、圓點、
原始碼清單、分欄、自訂指令等內容。
\end{abstract}

\tableofcontents
\newpage

%% ================================================================
\section{日文組版的基本}
%% ================================================================

\subsection{和文・歐文的混合}

pLaTeX 可以自然地混合日文與歐文。
例如「Hello, World!」或 \texttt{LaTeX2e} 類的
ASCII 文字會自動插入適當的空白。

全形字與半形字之間的間距調整由 pLaTeX 自動處理:
日本語English日本語、數字123日本語、記號\%日本語。

\subsection{Ruby(振假名)}

使用 \texttt{okumacro} 套件的 \verb|\ruby| 指令可以加上 Ruby(振假名)。

\begin{center}
\ruby{漢字}{かんじ}\ruby{情報処理}{じょうほうしょり}
\ruby{自然言語}{しぜんげんご}\ruby{処理}{しょり}
\end{center}

\subsection{圓點(傍點)}

使用 \texttt{okumacro}\verb|\kenten| 可以加上圓點(傍點)。

\begin{center}
\kenten{重要な箇所}には圏点を打つことができる。
\end{center}

\subsection{字體大小}

{\tiny 極小} {\scriptsize 腳註} {\footnotesize} {\small 稍小}
{\normalsize 標準} {\large 稍大} {\Large} {\LARGE 很大}
{\huge 超大} {\Huge 最大}

\subsection{文字裝飾}

\begin{itemize}
\item \textbf{粗體(Bold)}
\item \textit{斜體(英文字)}
\item \textsl{斜向}
\item \textsc{Small Caps}
\item \texttt{等寬字體(Typewriter)}
\item \underline{底線文字}
\item \textcolor{red}{紅色文字}
\item \textcolor{blue!70!black}{藍色文字}
\item \colorbox{yellow}{標示底色}
\end{itemize}

%% ================================================================
\section{文件結構}
%% ================================================================

\subsection{標題層級}

在 pLaTeX(\texttt{jarticle})中可以使用以下標題層級:
\verb|\section|、\verb|\subsection|、\verb|\subsubsection|、
\verb|\paragraph|、\verb|\subparagraph|。

\subsubsection{子子節範例}
這是子子節的範例。

\paragraph{段落標題}
這是段落標題的範例。接續的內文沒有縮排。

\subsection{腳註}

可以在內文中加入腳註\footnote{這是腳註的文字。會自動配置在頁底。}
也可以使用多個腳註\footnote{第二個腳註。編號會自動遞增。}

\subsection{交叉參照}

加上 \verb|\label| 之後,
可以用 \verb|\ref| 或 \verb|\pageref| 來參照。
例:下一節是第~\ref{sec:math}~節(第~\pageref{sec:math}~頁)。

%% ================================================================
\section{數學式環境}
\label{sec:math}
%% ================================================================

\subsection{行內數學式與顯示數學式}

行內數學式:$E = mc^2$$\alpha + \beta = \gamma$
$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$

顯示數學式:
\[
\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}
\]

\subsection{equation・align 環境}

帶編號的數學式(\texttt{equation}):
\begin{equation}
\label{eq:euler}
e^{i\pi} + 1 = 0
\end{equation}
式~\eqref{eq:euler} 為歐拉恆等式。

多行對齊(\texttt{align}):
\begin{align}
(a+b)^2 &= a^2 + 2ab + b^2 \\
(a-b)^2 &= a^2 - 2ab + b^2 \\
(a+b)(a-b) &= a^2 - b^2
\end{align}

\subsection{矩陣・向量}

\begin{equation}
A = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix},\quad
\det(A) = a_{11}a_{22} - a_{12}a_{21}
\end{equation}

\subsection{分段、分數、極限}

\begin{equation}
f(x) = \begin{cases}
x^2 & (x \geq 0) \\
-x^2 & (x < 0)
\end{cases}, \qquad
\lim_{n\to\infty} \left(1 + \frac{1}{n}\right)^n = e
\end{equation}

%% ================================================================
\section{表格(tabular 環境)}
%% ================================================================

\subsection{基本表格}

\begin{table}[h]
\centering
\caption{各城市氣溫資料(示意)}
\label{tab:temp}
\begin{tabular}{lrrr}
\toprule
城市 & 最高氣溫 (°C) & 最低氣溫 (°C) & 平均氣溫 (°C) \\
\midrule
東京 & 35.2 & 25.1 & 29.8 \\
大阪 & 36.4 & 26.3 & 31.0 \\
札幌 & 28.7 & 19.5 & 23.9 \\
那覇 & 32.1 & 27.4 & 29.8 \\
\bottomrule
\end{tabular}
\end{table}

\subsection{複雜表格(\texttt{array} 擴充・儲存格合併)}

\begin{table}[h]
\centering
\caption{程式語言比較}
\label{tab:lang}
\begin{tabular}{|l|c|c|c|}
\hline
語言 & 型別 & 範式 & 主要用途 \\
\hline\hline
Python & 動態 & 多範式 & AI/資料分析 \\
Rust & 靜態 & 系統程式 & 系統開發 \\
Haskell & 靜態 & 函數式 & 研究/金融 \\
JavaScript & 動態 & 多範式 & Web 前端 \\
\hline
\end{tabular}
\end{table}

像表~\ref{tab:temp} 與表~\ref{tab:lang} 可以透過 \verb|\label/\ref| 進行參照。

%% ================================================================
\section{列表環境}
%% ================================================================

\subsection{項目符號列表(itemize)}

\begin{itemize}
\item 第一個項目
\item 第二個項目
\begin{itemize}
\item 嵌套項目 A
\item 嵌套項目 B
\end{itemize}
\item 第三個項目
\end{itemize}

\subsection{編號列表(enumerate)}

\begin{enumerate}[(1)] % enumerate 套件指定格式
\item 最初的步驟
\item 下一個步驟
\item 最後的步驟
\end{enumerate}

\subsection{說明列表(description)}

\begin{description}
\item[pLaTeX] 支援日文的 \LaTeX 系統
\item[upLaTeX] 支援 Unicode 的 pLaTeX 後繼版
\item[LuaLaTeX] 可使用 Lua 腳本的 \LaTeX 系統
\end{description}

%% ================================================================
\section{verbatim・原始碼}
%% ================================================================

\subsection{verbatim 環境}

\begin{verbatim}
#include <stdio.h>
int main(void) {
printf("Hello, pLaTeX!\n");
return 0;
}
\end{verbatim}

\subsection{用 listings 做語法高亮}

\begin{lstlisting}[language=Python, caption={費波那契數列(Python)}]
def fibonacci(n: int) -> int:
"""使用遞迴計算費波那契數列"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)

# 輸出前 10 項
for i in range(10):
print(f"F({i}) = {fibonacci(i)}")
\end{lstlisting}

%% ================================================================
\section{框線・強調框}
%% ================================================================

可以使用 \texttt{ascmac} 套件的 \texttt{itembox}\texttt{screen} 等。

\begin{itembox}[l]{重要要點}
\begin{itemize}
\item pLaTeX 使用 \texttt{jarticle} / \texttt{jbook} 文件類別
\item 使用 \texttt{okumacro} 可以用 Ruby 與圓點
\item\texttt{platex + dvipdfmx} 產生 PDF
\end{itemize}
\end{itembox}

\vspace{5pt}

\begin{screen}
\texttt{screen} 環境:終端機風格的框線。用來顯示原始碼範例或輸出範例。
\end{screen}

%% ================================================================
\section{分欄}
%% ================================================================

可用 \texttt{multicol} 套件從某處開始切成兩欄。

\begin{multicols}{2}
\noindent
這是兩欄版面的左欄。
pLaTeX 也能自然處理日文的分欄。
即使是長文也會自動均分,
適合做報章雜誌風格的排版。

\columnbreak

\noindent
這是右欄。
可以用 \verb|\columnbreak| 強制換欄。
數學式 $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$
也可以行內使用。
\end{multicols}

%% ================================================================
\section{行距・間距調整}
%% ================================================================

\subsection{水平間距}

字與字之間的空格:a\,b(\verb|\,| 細空白)、
a\enspace b(\verb|\enspace|)、
a\quad b(\verb|\quad|)、
a\qquad b(\verb|\qquad|)。

\subsection{垂直間距}

可以用 \verb|\vspace| 新增垂直間距。

\vspace{5mm}
在此插入自上方 5mm 的垂直間距後的文字。
\vspace{5mm}

\subsection{行距}

{\setstretch{1.8}
此段落用 \texttt{setspace} 套件的
\texttt{setstretch} 將行距設為 1.8 倍。
在日文文件中,行距約 1.5〜2.0 較為易讀。
}

%% ================================================================
\section{自訂指令與環境}
%% ================================================================

\subsection{自訂指令定義}

\newcommand{\R}{\mathbb{R}}
\newcommand{\N}{\mathbb{N}}
\newcommand{\highlight}[1]{\colorbox{yellow!60}{#1}}
\newcommand{\term}[1]{\textbf{\textit{#1}}} % 用語強調

範例:$\R$(實數集)、$\N$(自然數)。
\highlight{被標示的文字}
\term{機器學習}(Machine Learning)是人工智慧的一個領域。

\subsection{自訂環境定義}

\newenvironment{mybox}[1]{%
\begin{center}\begin{tabular}{|p{0.85\linewidth}|}
\hline\vspace{1pt}
\textbf{#1}\\[2pt]
}{%
\\\hline\end{tabular}\end{center}
}

\begin{mybox}{自製的框線}
使用 \verb|\newenvironment| 可以定義自訂環境。
可以接受參數以利彈性重複使用。
\end{mybox}

%% ================================================================
\section{參考文獻}
%% ================================================================

文獻引用可用 \verb|\cite| 指令進行~\cite{knuth1984,lamport1994,okumura2020}
文件末會輸出參考文獻清單。

%% ================================================================
%% 參考文獻清單(thebibliography 環境)
%% ================================================================
\begin{thebibliography}{99}

\bibitem{knuth1984}
D.~E. Knuth,
\textit{The \TeX book},
Addison-Wesley, 1984.

\bibitem{lamport1994}
L.~Lamport,
\textit{\LaTeX: A Document Preparation System}, 2nd ed.,
Addison-Wesley, 1994.

\bibitem{okumura2020}
奥村晴彦・黒木裕介,
\LaTeXe 美文書作成入門』第 8 版,
技術評論社, 2020.

\end{thebibliography}

\end{document}

編譯流程

基本(platex + dvipdfmx)

為了正確解決交叉參照與目錄,請先執行 platex 兩次,然後用 dvipdfmx 轉成 PDF。

platex -interaction=nonstopmode -kanji=utf8 document.tex
platex -interaction=nonstopmode -kanji=utf8 document.tex
dvipdfmx document.dvi

ptex2pdf

也可以用將 platex + dvipdfmx 二步驟合併成一個指令的 ptex2pdf

基本指令

ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex
選項含意
-l使用以 latex 為基礎的引擎(platex)
-ot "..."傳給 platex 的選項
-kanji=utf8指定輸入編碼為 UTF-8
-interaction=nonstopmode發生錯誤時不停止繼續處理

步驟

  1. 執行 ptex2pdf 兩次(為了解決交叉參照與目錄)

    ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex
    ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex
  2. 確認結果

    • 成功:會產生 document.pdf
    • 發生錯誤:檢查 .log 中以 ! 開頭的行及其前後 3 行
    • 只有警告:檢查 Overfull/Underfull hbox、未定義參照等
  3. 若需要 BibTeX(當有 .bib 檔或包含 \bibliography 指令時)

    ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex
    bibtex document
    ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex
    ptex2pdf -l -ot "-kanji=utf8 -interaction=nonstopmode" document.tex

    在執行 bibtex 後再次執行 ptex2pdf 兩次是為了解決參考文獻清單與內文引用編號的交叉參照。

常見錯誤與處理

Undefined control sequence

! Undefined control sequence.
l.42 \somecommand

代表使用了未定義的指令。通常是忘了載入某個套件或是打錯字造成的。

Missing $ inserted

! Missing $ inserted.

表示在數學模式外使用了 _(下標)或 ^(上標)等數學符號。請用 $...$ 將其包起來。

File not found

! LaTeX Error: File `image.png' not found.

表示 \includegraphics 指定的檔案路徑錯誤。請確認相對於 .tex 的路徑是否正確。

Overfull \hbox

Overfull \hbox (12.3pt too wide) in paragraph at lines 55--60

這是警告不是錯誤。表示行超出指定寬度。常見原因為長 URL 或長英文字,通常可用 \allowbreak 或用 \url{}(url 套件)來解決。

檢查日誌

編譯後產生的 .log 檔會記錄錯誤與警告的詳細資訊。錯誤行通常以 ! 開頭,警告則包含 Warning 字樣。

grep -n "^!" document.log # 抽出錯誤行
grep -n "Warning" document.log # 抽出警告行

刪除中間檔案

編譯會產生許多中間檔。若想保留 PDF 並清理其他檔案,可執行下列指令。

rm -f document.aux document.log document.dvi \
document.toc document.lof document.lot \
document.out document.bbl document.blg \
document.synctex.gz

若要全部刪除(包含 PDF),請再加上 .pdf