0%

正威網路行銷工作室,專注於基隆、台北與全台客製化網頁設計,提供專業 RWD 響應式、SEO 搜尋引擎優化、品牌形象設計與網站維護託管服務。

搜尋文章
聯絡資訊
地址基隆市仁二路 220 號 5 樓
追蹤我們

PHP 點擊計數器:用 txt 檔記錄連結被點了幾次的安全寫法(附 GA4 替代做法)

iframe自動適應高度

想在「下載型錄」「立即報名」這類連結旁邊顯示「已經有 123 人點過」,又不想為了一個數字建立資料庫,最簡單的方式是用 PHP 把次數存在一個 txt 檔裡。

先說清楚:如果你只是想「知道」哪些連結被點了幾次,不需要公開顯示在網頁上,用 Google Analytics 4(GA4)更準確、也更省事,做法在文章後面。需要把數字顯示給訪客看的時候,再用下面的程式。

舊版程式碼的問題

這篇文章舊版的程式碼,是點擊 ?click=yes 的連結時,讀取 txt 裡的數字、加 1 再寫回去。概念很簡單,但有幾個問題:

  • 同時有人點擊會漏算:兩個人同時點擊,兩邊都讀到 100、都寫回 101,實際上少算了一次。需要檔案鎖定,讓寫入排隊進行。
  • 用網址參數計數:任何人重新整理 ?click=yes 的網址就能一直灌次數;搜尋引擎爬蟲、通訊軟體產生連結預覽時,也可能去開這個網址,莫名其妙多算。會改變資料的動作應該用 POST,而不是一般的連結。
  • PHP 8 會出現警告:沒有帶參數打開頁面時,$_GET['click'] 不存在,PHP 8 會顯示「Undefined array key」的警告。
  • 計數檔放在網站公開目錄:任何人都能直接打開 txt 檔,甚至被其他程式寫入。

新版寫法:三個檔案

  • counter-lib.php:讀取與增加次數的共用函式。
  • count.php:接收點擊、把次數加 1(只接受 POST)。
  • 你的網頁:顯示次數,訪客點擊時通知 count.php。

計數檔建議放在網站公開目錄以外的資料夾。例如網站根目錄是 public_html,就在它的上一層建立 counter-data 資料夾,並確認 PHP 有權限寫入。下面的範例假設三個檔案都放在 public_html 裡。

第一個檔案:counter-lib.php

PHP
<?php
declare(strict_types=1);

// 允許計數的項目:只有這裡列出的名稱才會被計數
const COUNTER_IDS = ['catalog', 'price-list'];

// 計數檔存放的資料夾:放在網站公開目錄的外面
const COUNTER_DIR = __DIR__ . '/../counter-data';

function counter_file(string $id): string
{
    if (!in_array($id, COUNTER_IDS, true)) {
        throw new InvalidArgumentException('不允許的計數項目');
    }
    return COUNTER_DIR . '/' . $id . '.txt';
}

function counter_read(string $id): int
{
    $file = counter_file($id);
    if (!is_file($file)) {
        return 0;
    }
    $fp = fopen($file, 'r');
    if ($fp === false) {
        return 0;
    }
    flock($fp, LOCK_SH); // 共用鎖定:等寫入完成才讀取
    $count = (int) stream_get_contents($fp);
    flock($fp, LOCK_UN);
    fclose($fp);
    return $count;
}

function counter_increment(string $id): int
{
    $fp = fopen(counter_file($id), 'c+'); // 檔案不存在就建立,存在也不會清空
    if ($fp === false) {
        throw new RuntimeException('無法開啟計數檔');
    }
    try {
        if (!flock($fp, LOCK_EX)) { // 獨佔鎖定:同時有人點擊時排隊處理
            throw new RuntimeException('無法鎖定計數檔');
        }
        $count = (int) stream_get_contents($fp) + 1;
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, (string) $count);
        fflush($fp);
        flock($fp, LOCK_UN);
        return $count;
    } finally {
        fclose($fp);
    }
}

幾個重點:

  • COUNTER_IDS 列出允許計數的項目。檔名只會從這份清單產生,訪客送來的名稱不在清單裡就直接拒絕,就不會有人用 ../ 之類的路徑去讀寫其他檔案。
  • flock 加上 LOCK_EX 是獨佔鎖定:同時有好幾個人點擊時,會一個一個輪流「讀取、加 1、寫回」,不會互相覆蓋。
  • fopen 的 c+ 模式:檔案不存在就建立,存在時不會先清空內容。

第二個檔案:count.php

PHP
<?php
declare(strict_types=1);

require __DIR__ . '/counter-lib.php';

// 只接受 POST,一般開網址 (GET) 不會計數
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    http_response_code(405);
    header('Allow: POST');
    exit;
}

// 驗證輸入:必須是字串,而且在允許的清單裡
$id = $_POST['id'] ?? '';
if (!is_string($id) || !in_array($id, COUNTER_IDS, true)) {
    http_response_code(400);
    exit;
}

try {
    counter_increment($id);
    http_response_code(204); // 成功,不需要回傳內容
} catch (Throwable $e) {
    error_log('counter: ' . $e->getMessage());
    http_response_code(500);
}

第三個檔案:你的網頁

網頁要是 .php 檔,才能讀取並顯示目前的次數:

PHP
<?php
declare(strict_types=1);

require __DIR__ . '/counter-lib.php';

$catalogCount = counter_read('catalog');
?>
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>點擊計數範例</title>
</head>
<body>
  <p>
    <a href="/files/catalog.pdf" data-counter="catalog">下載產品型錄</a>
    (已下載 <span data-counter-value="catalog" data-count="<?= $catalogCount ?>"><?= number_format($catalogCount) ?></span> 次)
  </p>

  <script>
    document.querySelectorAll('a[data-counter]').forEach((link) => {
      link.addEventListener('click', () => {
        const id = link.dataset.counter;
        navigator.sendBeacon('/count.php', new URLSearchParams({ id: id }));

        // 畫面上的數字先加 1 (實際次數以伺服器記錄為準)
        const output = document.querySelector('[data-counter-value="' + id + '"]');
        if (output) {
          const next = Number(output.dataset.count) + 1;
          output.dataset.count = String(next);
          output.textContent = next.toLocaleString();
        }
      });
    });
  </script>
</body>
</html>
  • 連結本身是一般的連結,沒有 JavaScript 也能正常開啟。
  • navigator.sendBeacon 會在背景用 POST 送出計數,就算連結馬上跳到別的頁面,這個請求也會送出,不會拖慢連結的開啟。
  • 搜尋引擎爬蟲通常不會觸發點擊事件,不會被算進去。
  • 要增加其他計數項目,先加進 counter-lib.php 的 COUNTER_IDS,再在連結加上對應的 data-counter。

這個寫法的限制

  • 用滑鼠中鍵、或按右鍵選「在新分頁中開啟」時,不會觸發 click 事件,這些點擊不會被計算。
  • 任何人都能自己送 POST 到 count.php 灌次數。這個數字適合當作參考,不適合當作正式統計或抽獎的依據。
  • 少數主機的檔案系統(例如部分網路磁碟)上,flock 可能沒有作用。流量大、或計數項目很多的網站,建議改用資料庫,一行 SQL 就能安全地加 1(程式裡請用預處理語句帶入 id):
SQL
UPDATE click_counters SET clicks = clicks + 1 WHERE id = 'catalog';

只想知道點擊數據?用 GA4 更好

如果目的只是想知道「型錄被下載了幾次」「外部連結被點了幾次」,GA4 比自己寫計數器更適合:

  • 不用寫程式就有的資料:GA4 的「加強型評估」預設就會記錄外連點擊(連到其他網站的連結)與檔案下載(PDF、Word、Excel、壓縮檔等常見格式),在事件報表裡可以看到 click、file_download 這兩個事件。
  • 追蹤站內特定按鈕:可以用 Google 代碼管理工具(GTM)設定點擊觸發條件;網站已經安裝 GA4 的 gtag.js 的話,也可以在點擊時送出自訂事件:
JavaScript
gtag('event', 'catalog_click');
  • 資料比較乾淨:GA4 會自動排除已知的機器人流量,還能看到點擊的人從哪裡來、用什麼裝置,比單純的數字更有參考價值。

GA4 的限制是數字無法直接顯示在網頁上給訪客看。所以最常見的搭配是:對內分析用 GA4,要公開顯示數字時才用本篇的計數器。

延伸閱讀

哪些按鈕有人點、哪些內容沒人看,是調整網站內容最實在的依據。正威網路行銷是 25 年經驗的基隆網頁設計工作室,也提供 SEO 搜尋引擎優化與 Google 關鍵字廣告服務,歡迎來聊聊。

分享這篇文章

返回文章列表更多「html網頁語法教學」文章

發表留言

還沒有人留言,歡迎第一個分享你的想法或問題。留言經審核後才會顯示,Email 不會公開。

讓我們一起打造
您的專屬網站!