chundev
日期:2026-05-30 情境:用純文字 QR Code 承載一段資訊,希望「掃了直接顯示」
把一段含長數字(如編號 123456789)的純文字做成 QR Code,iPhone 相機掃描後不顯示文字,反而跳出撥打電話的選項。根因是手機相機的資料偵測器(iOS 的 NSDataDetector、Android 的 Smart Linkify / Google Lens)會把「連續 7 碼以上的數字」判定成電話號碼。
關鍵雷點:把數字改成全形(123)沒用、用空格或 - 分隔也沒用。 唯一可靠的解法是在數字串中間插入「非數字、且不是電話分隔符」的字元(中文字或英文字母),讓任何一段連續的半形數字都不足 7 碼。
QR Code 本身只是一段靜態資料。手機相機掃到內容後,作業系統會主動對這段文字做「資料偵測」,命中電話、網址、日期、地址等樣式時就提供對應的快捷動作。
tel: 開頭 → 跳撥號http(s):// 開頭 → 開瀏覽器第三種就是這次踩到的坑:明明是純文字,卻被「智慧」辨識成電話。
QR 內容(純文字):
編號:123456789
名稱:DEMO
狀態:進行中
...
iPhone 掃描結果:
→ 不顯示文字,跳出「撥打 123456789」的提示
最有搜尋價值的關鍵字:QR code plain text iPhone wants to call / 純文字 QR 掃描跳撥號。
| 嘗試 | 做法 | 結果 | 原因 |
|---|---|---|---|
| ❌ 1 | 把編號改成全形數字 123456789 |
仍跳撥號 | 偵測器在比對前會做 NFKC 正規化,把全形相容字元折回半形 ASCII,等於沒改 |
| ❌ 2 | 用空格/連字號分隔 123-456-789、123 456 789 |
仍跳撥號 | 電話號碼解析器(libphonenumber 那套)把空格、-、.、()、+ 都視為合法分隔符,照樣串成一個號碼 |
| ✅ 3 | 在數字中插入中文字或英文字母 123年456789號 |
正常顯示文字 | 中文字/字母不是數字也不是電話分隔符,連續半形數字被切到不足 7 碼,偵測器不再命中 |
原則:QR 純文字中,任何一處都不能出現約 7 碼以上的連續半形數字。
要打斷數字串,插入的字元必須同時滿足:
- / . / ( ) / + 都無效)✅ 有效的打斷字元:中文字、英文字母、/(部分情境)
❌ 無效:全形數字、空格、-、.、()、+
範例(Python,用 segno):
import segno, re
# 把長編號用語意化方式打斷,例如 "123456789" → "123年456789號"
content = "編號:123年456789號\n名稱:DEMO\n狀態:進行中"
# 自我驗證:全文不可有 >=7 碼連續半形數字
assert not re.findall(r"\d{7,}", content), "仍有長數字串會被當電話"
segno.make(content, error="m").save("out.png", scale=10, border=4)
⚠️ 連「括號註記原始值」都不行——只要文字裡再出現一次完整的 9 碼數字,就會再次觸發。原始數值要保留就放在系統內,別印進 QR。
這個 assert 是關鍵:把「不可有長數字串」變成可程式驗證的條件,產生前先檢查,比掃描後才發現失敗可靠得多。
像 5/17/2026, 12:11:42 PM 這種時間戳,因為被 /、:、,、空格切成多個短段,沒有 7 碼以上連續數字,不會被判定成電話。但它可能被偵測成「日期」而提供「加入行事曆」——若連這也不想要,同樣用非數字字元打斷即可。
順手量了繁體中文的容量。QR 中文走 byte mode(UTF-8,每字 3 bytes),容量隨糾錯等級遞減(以最大 Version 40 計):
| 糾錯等級 | 容錯能力 | 繁中字上限 |
|---|---|---|
| L | ~7% | 984 字 |
| M(常用預設) | ~15% | 777 字 |
| Q | ~25% | 554 字 |
| H | ~30% | 424 字 |
⚠️ 兩個容易誤判的點:
鱻、龘)不在 Shift-JIS,會退回 byte mode。規劃容量時一律以 byte mode(3 bytes/字)為準才安全。NSDataDetector / Android Smart Linkify 的預設行為,純文字一樣會被加工。-、.、()、+ 全都算,所以這些都打斷不了。\d{7,}),在產生階段就攔截,比靠人工掃描測試可靠。