從 PDF 複製的文字有奇怪的空格和換行符

PDF 裡複製出來的中文,有時候文字之間會出現不自然的空格和換行符,還有數字和英文字母都是全形的、標點符號全是半形的。 例如:

1969 年,阿 波 羅11
號(Apollo 11)登
陸 月 球。

看起來很詭異。

我需要一個工具,在貼上的那一刻自動把文字「洗」乾淨,變成:

1969年,阿波羅11號(Apollo 11)登陸月球。

要做的事情包括:

  • 刪除漢字之間的空格和換行符
  • 把全形數字(0123)轉換為半形數字(0123)
  • 把全形字母(Apollo)轉換為半形數字(Apollo)
  • 把半形標點(!?"":;)轉換為全形標點(!?“”:;)

我的電腦是 Windows。不知道 Mac 是不是有更方便的方法解決這個問題。 總之用 AutoHotkey v2 寫了個快捷方式。 AutoHotkey 是一個輕量級自動化工具,可以開機自啟、常駐後臺執行。

詳細來說的話就是安裝 AutoHotkey,然後随便在哪里新建一個 .ahk 文件1,寫入程式碼。(if 有人想要完整程式碼,請看後面。) 雙擊執行該 .ahk 檔案。該工具會在背景執行。也可以把這個 .ank 檔案設定為開機自啟,跳過雙擊執行這一步。

現在我只需要 複製 PDF 中的文本,貼上的時候不要按 Ctrl+V,而是按我自己設定的快捷鍵(Ctrl+Shift+Q),剪貼簿裡的文字會被自動“清洗”、自動貼上。 貼上的速度和用 Ctrl+V 一樣快,但貼出來的文字變“乾淨”了。


今天改進了一下。 之前的設計是遇到空格就刪掉:

text := RegExReplace(text, "\s+")

然後發現正常英文之間的空格它也刪除,比如

  • 漢字排版測試(Typography Test) 在轉換之後會變成 漢字排版測試(TypographyTest)Typography Test 中間的空格消失了。
  • 阿波羅11號(Apollo 11)在轉換之後會變成 阿波羅11號(Apollo11)Apollo 11 之間的空格消失了。

遂改成 只刪除「空格兩側至少有一側是中日韓字元」的空格。 如果空格兩側都是純 ASCII 字母或數字(說明是正常的英文/數字間隔),就保留。


現在唯一沒做的是小數點.轉句號

因為需要考慮的特殊條件有點太多了。 曾經嘗試設定轉換規則為,只有當小數點.前面緊接著中文/全形字元,而且後面不是緊接數字、英文字母或另一個小數點時,才轉換成全形句號 。比如:

  • 阿波羅11號登上月球. 小數點前是中文字元,轉換成 阿波羅11號登上月球。
  • 0.99cm 小數點前是數字,不轉換
  • 1.第一項 小數點前是數字,不轉換
  • www.wikipedia.org,小數點前是字母,不轉換
  • 这样啊...,小數點後是小數點,不轉換
  • 一.1990年度工作報告,小數點後是數字,不轉換

但是中文文章也會出現句尾是英文,或者句首是英文的情況。比如 不可使用生成式AI. ,小數點前是英文,不轉換,但實際上應該使用中文句號。 情況太複雜了!所以沒有在標點轉換中寫入轉換小數點。

完整 AHK 代码

#Requires AutoHotkey v2.0

; 觸發這個程式的快捷鍵 是 ^+q 
; ^+q 的意思是 shift+ctrl+q

^+q::
{
    text := A_Clipboard

    if (text = "")
        return

    ; 1. 全形數字轉半形
    fullwidthNum := "0123456789"
    halfwidthNum := "0123456789"
    Loop Parse, fullwidthNum
        text := StrReplace(text, A_LoopField, SubStr(halfwidthNum, A_Index, 1))

    ; 2. 全形字母轉半形字母(A-Z, a-z):引用函式
    text := ConvertFullwidthLetters(text)

    ; =========================
    ; 3. 半形標點轉全形標點
    ; =========================

    ; 一般標點:一對一直接替換
    text := StrReplace(text, ",", ",")
    text := StrReplace(text, "!", "!")
    text := StrReplace(text, "?", "?")
    text := StrReplace(text, ":", ":")
    text := StrReplace(text, ";", ";")
    text := StrReplace(text, "~", "~")
    text := StrReplace(text, "(", "(")
    text := StrReplace(text, ")", ")")
    text := StrReplace(text, "%", "%")

    ; 雙引號 " 和 單引號 ' :需要根據出現順序交替變成左/右引號
    text := ConvertQuotes(text, '"', "“", "”")
    text := ConvertQuotes(text, "'", "‘", "’")

    ; =========================
    ; 4. 刪除空白字元
    ; 只刪除「與中文/全形字元相鄰」的空白
    ; =========================
    
    ; 中日韓統一表意文字 + 中日韓擴展A + 中日韓兼容表意文字 + 中文標點區(、。《》「」等)
    CJK := "\x{4E00}-\x{9FFF}\x{3400}-\x{4DBF}\x{F900}-\x{FAFF}\x{3000}-\x{303F}"

    text := RegExReplace(text, "(?<=[" CJK "])\s+")   ; 空白前面是中文/全形字元 → 刪除
    text := RegExReplace(text, "\s+(?=[" CJK "])")    ; 空白後面是中文/全形字元 → 刪除

    ; 寫回剪貼簿並貼上
    A_Clipboard := text

    ClipWait(1)

    Send("^v")
}

; =========================
; 全形字母轉半形字母函式
; 全形 A-Z: U+FF21-FF3A → 半形 A-Z: U+0041-005A
; 全形 a-z: U+FF41-FF5A → 半形 a-z: U+0061-007A
; =========================
ConvertFullwidthLetters(text) {
    result := ""
    Loop Parse, text
    {
        code := Ord(A_LoopField)
        if (code >= 0xFF21 && code <= 0xFF3A) {
            ; 全形大寫字母
            result .= Chr(code - 0xFF21 + 0x41)
        } else if (code >= 0xFF41 && code <= 0xFF5A) {
            ; 全形小寫字母
            result .= Chr(code - 0xFF41 + 0x61)
        } else {
            result .= A_LoopField
        }
    }
    return result
}

; =========================
; 引號轉換輔助函式
; 把半形引號 quoteChar 按出現順序交替替換為 leftChar / rightChar
; =========================
ConvertQuotes(text, quoteChar, leftChar, rightChar) {
    result := ""
    isLeft := true
    Loop Parse, text
    {
        if (A_LoopField = quoteChar) {
            result .= isLeft ? leftChar : rightChar
            isLeft := !isLeft
        } else {
            result .= A_LoopField
        }
    }
    return result
}

  1. 如果不知道怎樣新建 .ahk 文件:新建一個純文字檔案(新建文件.txt),把程式碼寫在裡面;儲存檔案並關閉檔案;然後將該檔案的副檔名從 .txt 更改為 .ahk。好了。 ↩︎

?隨機抽選一篇