Learning Objectives
- 分辨單引號、雙引號、heredoc、nowdoc,在雙引號字串裡用 {$arr['k']} 插入陣列元素
- 說明中文字在 UTF-8 佔 3 個位元組,處理中文時改用 mb_strlen、mb_substr
- 用 strpos 搜尋時以 !== false 判斷,並使用 str_contains、str_starts_with、str_ends_with
- 使用 str_replace、trim、explode、implode、str_pad、sprintf、number_format 整理與格式化字串
- 用 preg_match、preg_match_all、preg_replace、preg_split 處理有規則的文字,中文要加 u 修飾字
網頁裡幾乎都是字串
PHP 最常做的事就是產生文字:HTML、表單送來的姓名與留言、要寫進檔案的紀錄、要寄出的通知。 使用者的輸入一律是字串,前後可能有多餘的空白、格式可能不對;要顯示的數字需要加上千分位、補零、固定小數位數。 這一課整理處理字串的常用工具:
| 需求 | 函式 |
|---|---|
| 長度、取一段 | strlen、mb_strlen、substr、mb_substr |
| 搜尋 | strpos、str_contains、str_starts_with、str_ends_with |
| 取代、去空白、大小寫 | str_replace、trim、strtoupper、ucfirst |
| 切割與合併 | explode、implode、str_split |
| 格式化 | sprintf、printf、number_format、str_pad |
| 有規則的文字 | preg_match、preg_replace、preg_split |
PHP 的字串其實是一串位元組(byte),處理中文時要特別注意,本課會一再看到這一點。
四種寫字串的方式
| 寫法 | 跳脫字元(\n、\t…) |
變數插值 | 用途 |
|---|---|---|---|
'單引號' |
只認得 \' 和 \\ |
不處理 | 固定的文字 |
"雙引號" |
處理 | 處理 | 要插入變數、要換行 |
heredoc <<<END … END |
處理 | 處理 | 很多行的文字、HTML |
nowdoc <<<'END' … END |
不處理 | 不處理 | 很多行、要原樣保留 |
雙引號裡插入變數的寫法:
"Hi $name" // 簡單的變數
"{$name}你好" // 變數後面緊接中文或字母時,用大括號標出範圍
"{$item['title']}" // 陣列元素:用大括號,鍵照常加引號
"{$user->city}" // 物件的屬性(物件在第 14 課介紹)
"${name}" 這種寫法在 PHP 8.2 起已棄用(deprecated),請用 {$name}。
字串的串接用 .(不是 +),$s .= 'x' 把 'x' 接到 $s 後面。
單雙引號、插值與 heredoc
第 5 行用單引號,$name 和 \n 都原樣印出;第 6 行換成雙引號,就變成 Amy 和換行。第 16 行的 heredoc 結尾標記 HTML; 前面有 4 個空白,PHP 會把每一行開頭同樣的 4 個空白去掉(PHP 7.3 起)。
中文字串的長度與擷取
網頁一律使用 UTF-8 編碼:英文字母、數字佔 1 個位元組,中文字佔 3 個位元組(少數罕用字 4 個)。 PHP 傳統的字串函式都以位元組計算,所以處理中文會出錯:
| 以位元組計算 | 以字計算(多位元組,mb_) |
|---|---|
strlen('程式設計') 是 12 |
mb_strlen('程式設計') 是 4 |
substr($s, 0, 4) 切到第二個字的一半,產生亂碼 |
mb_substr($s, 0, 2) 取前兩個字 |
strtoupper 只轉換英文字母 |
mb_strtoupper 也處理其他語言的字母 |
驗證「姓名最多 20 個字」要用 mb_strlen;擷取標題前 30 個字當摘要要用 mb_substr。
substr($s, -5) 的負數表示從後面數過來,mb_substr 也一樣。
strlen 與 mb_strlen、substr 與 mb_substr
第 12 行用 substr 取「程式設計」的前 4 個位元組:第一個字 3 個位元組,再加上第二個字的第 1 個位元組,於是 $broken 的結尾是半個字,左邊的變數區顯示成 �。bin2hex 把每個位元組印成兩位十六進位數字,可以看到確實只有 4 個位元組。
想一想:引號與長度
下面的程式會印出什麼?
<?php
$n = 3;
echo 'n=$n', " n=$n", " {$n}0";
下面的程式會印出什麼?
<?php
echo strlen('臺北101'), ' ', mb_strlen('臺北101');
搜尋:0 和 false 不一樣
strpos($s, $find) 回傳 $find 第一次出現的位置(從 0 起算),找不到回傳 false。
問題是:如果在開頭找到,位置是 0,而 0 當成條件是 false(第 05 課):
if (strpos('php is fun', 'php')) { … } // 錯:在位置 0 找到,卻被當成沒找到
if (strpos('php is fun', 'php') !== false) { … } // 對:用嚴格比較區分 0 和 false
只想知道「有沒有」的時候,PHP 8.0 起可以直接用 str_contains($s, $find),
另外有 str_starts_with、str_ends_with,都回傳 true 或 false,不會有這個問題。
其他常用的函式:
| 函式 | 作用 |
|---|---|
str_replace($find, $replace, $s, $count) |
取代全部;第四個參數(可省略)記下取代了幾次。不分大小寫用 str_ireplace |
explode(',', $s) |
依分隔字串切成陣列 |
implode('、', $arr) |
用分隔字串把陣列接成一個字串 |
str_split($s, 4) |
每 4 個位元組切一段(中文用 mb_str_split) |
str_pad($s, 長度, 補的字, 方向) |
補到指定長度,STR_PAD_LEFT 補在左邊 |
str_repeat('=', 5) |
重複 5 次 |
strpos 的陷阱、取代、切割與補齊
php 在字串的開頭,strpos 回傳 0。第 5 行的 if ($pos) 把 0 當成 false,走進了 else;第 10 行改用 !== false 才判斷正確。第 16 行傳入 $count,函式執行完後它記錄取代了幾次。
格式化:sprintf、printf、number_format
sprintf(格式, 值1, 值2, …) 依照格式字串產生一個字串;printf 用法相同,但直接印出。
格式字串裡的 % 開頭是佔位符,依序換成後面的值:
| 佔位符 | 意思 | 範例 | 結果 |
|---|---|---|---|
%s |
字串 | sprintf('%s 分', 'A') |
A 分 |
%d |
整數(小數直接捨去) | sprintf('%d', 95.8) |
95 |
%.2f |
浮點數,小數 2 位(四捨五入) | sprintf('%.2f', 87.456) |
87.46 |
%05d |
寬度 5,不足補 0 | sprintf('%05d', 42) |
00042 |
%8s、%-8s |
寬度 8,靠右/靠左(-) |
sprintf('[%-6s]', 'ab') |
[ab ] |
%% |
印出 % 本身 | sprintf('%d%%', 95) |
95% |
寬度以位元組計算,中文對不齊,表格對齊請用英數。
number_format(1234567.891) 加上千分位並四捨五入到整數(1,234,568),第二個參數指定小數位數。
比較字串:== 和 === 都區分大小寫;不分大小寫用 strcasecmp。strcmp($a, $b) 依字典順序比較,
$a 在前回傳負數、相同回傳 0、在後回傳正數(PHP 8.2 起固定是 -1、0、1),很適合拿來排序。
注意 == 比較兩個看起來像數字的字串時,會當成數字比較:'10' == '1e1' 是 true,要精確比對字串請用 ===。
sprintf、printf、number_format 與字串比較
對照每一行的格式字串與輸出區的結果:%.2f 四捨五入到兩位、%05d 補零、%-8s 靠左。最後三行比較字串,注意 '10' == '1e1' 是 true:兩個字串都像數字,== 把它們當成 10 來比較。
想一想:搜尋、切割與格式化
下面的程式會印出什麼?
<?php
$s = 'apple';
if (strpos($s, 'a')) {
echo 'yes';
} else {
echo 'no';
}
下面的程式會印出什麼?
<?php
echo count(explode(',', 'a,,b,')), ' ', implode('+', explode(' ', 'x y z'));
下面的程式會印出什麼?
<?php
printf('%05.1f|%-4s|%3d', 3.14159, 'ab', 7);
正規表達式入門
要找的不是固定的文字,而是「有規則的文字」(日期、Email、訂單編號)時,用正規表達式(regular expression)描述規則。
PHP 的寫法是把規則放在兩個斜線之間:'/規則/修飾字'。
| 規則 | 意思 |
|---|---|
\d |
一個數字;\s 空白;\w 英文字母、數字或底線 |
[A-Z]、[a-z0-9_] |
方括號裡任一個字元 |
. |
任一個字元 |
{4}、{2,4}、+、*、? |
重複 4 次、2 到 4 次、1 次以上、0 次以上、0 或 1 次 |
^、$ |
字串的開頭、結尾(整個字串都要符合時使用) |
( … ) |
群組:把符合的部分另外擷取出來 |
| 函式 | 作用 |
|---|---|
preg_match($re, $s, $m) |
找第一個符合的;找到回傳 1、沒找到 0;$m[0] 是整段,$m[1] 起是各個群組 |
preg_match_all($re, $s, $all) |
找全部,回傳個數;$all[0] 是所有符合的字串 |
preg_replace($re, $replace, $s) |
取代;$1、$2 代表群組的內容 |
preg_split($re, $s) |
依符合的位置切成陣列 |
處理中文一定要加 u 修飾字(/…/u):沒有 u 時,. 代表一個位元組,一個中文字會被當成 3 個字元。
規則寫在單引號字串裡,\d 這類反斜線就不必再跳脫。
擷取日期與訂單編號、取代、切割
第 4 行的規則有三個群組,右邊的 $m 會依序放整段日期、年、月、日。第 12 行比較有沒有 u 修飾字:「密碼」兩個字,加 u 換成 2 個星號,沒加 u 換成 6 個(每個位元組一個)。
想一想:正規表達式
下面的程式會印出什麼?
<?php
preg_match('/(\w+)@(\w+)\.com/', '聯絡:[email protected]', $m);
echo $m[2], ' ', count($m);
動手做:遮蔽個人資料
$raw 是從表單收到的名單,每行是「姓名,手機」,前後可能有多餘的空白。請逐行印出遮蔽後的資料:
- 姓名只保留第一個字,其餘每個字換成
*(「王小明」變成「王**」) - 手機保留前 4 碼與後 3 碼,中間換成
*** - 每行前面加上編號,格式是
1. 王** 0912***678
重點回顧
- 單引號原樣輸出;雙引號處理
\n等跳脫字元與變數插值;heredoc 像雙引號、nowdoc 像單引號 - 插入陣列元素或變數後面緊接文字時,寫成
{$item['title']}、{$name};串接用.與.= - UTF-8 的中文字佔 3 個位元組:計算字數用
mb_strlen、擷取用mb_substr,substr可能切出亂碼 strpos在開頭找到時回傳 0,判斷要用!== false;只問有沒有就用str_contains(PHP 8.0)str_replace、trim、explode、implode、str_pad、str_repeat回傳新字串,原字串不變sprintf/printf:%s、%d、%.2f、%05d、%-10s、%%;number_format加千分位==比較兩個數字形式的字串時會當成數字;精確比對用===,不分大小寫用strcasecmp- 正規表達式:
preg_match擷取、preg_match_all找全部、preg_replace取代、preg_split切割;有中文就加u