首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

人大復印報刊資料處理系統(匯入篇):檔案解析、正則提取、去重邏輯與批次匯入完整實作

     編輯:PHP 綜合知識

系統流程概覽

本篇實作核心匯入邏輯:上傳驗證 → 檔案讀取 → 正則分割文章 → 欄位提取 → 日期標準化 → 去重檢查 → 批次寫入 → 索引生成 → 結果回報。程式碼採用現代 PHP (嚴格型別、PDO、服務類、異常處理),修正原始碼中的編碼假設、SQL 注入、N+1 查詢、記憶體洩漏等問題。

匯入控制器:ImportController.php

<?php
// public/import.php
declare(strict_types=1);

require_once __DIR__ . '/../vendor/autoload.php';
$config = require __DIR__ . '/../config/config.php';

use App\Service\ImportService;
use App\Lib\ErrorHandler;

try {
    // 1. 建立 PDO 連線
    $dsn = "mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['name']};charset={$config['db']['charset']}";
    $pdo = new PDO($dsn, $config['db']['user'], $config['db']['pass'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    // 2. 處理 POST 匯入請求
    if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && isset($_FILES['upload_file'])) {
        $service = new ImportService($pdo, $config);
        $result = $service->processUpload($_FILES['upload_file'], $_POST);
        
        // 輸出結果
        echo "<h1>匯入完成</h1>";
        echo "<p>新增: {$result['inserted']} 筆 | 重複略過: {$result['duplicates']} 筆 | 總計: {$result['total']} 筆</p>";
        if ($result['index_file']) {
            echo "<p>索引檔: <a href='{$config['paths']['index_url']}{$result['index_file']}' target='_blank'>檢視</a></p>";
        }
        if (!empty($result['errors'])) {
            echo "<h3>錯誤/警告</h3><ul>";
            foreach ($result['errors'] as $err) echo "<li>" . htmlspecialchars($err) . "</li>";
            echo "</ul>";
        }
        exit;
    }

    // 3. 顯示上傳表單
    include __DIR__ . '/../templates/import_form.php';

} catch (\Throwable $e) {
    ErrorHandler::dbError($pdo ?? null, 'ImportController', $e);
}

核心服務類:ImportService.php

<?php
// src/Service/ImportService.php
namespace App\Service;

use PDO;
use SplFileObject;

class ImportService
{
    private PDO $pdo;
    private array $config;
    private array $patterns;

    public function __construct(PDO $pdo, array $config)
    {
        $this->pdo = $pdo;
        $this->config = $config;
        $this->compilePatterns();
    }

    // 編譯所有正則 (效能優化:僅編譯一次)
    private function compilePatterns(): void
    {
        $this->patterns = [
            'split'       => '/\n\s+\n\s+\n\s+/',           // 文章分隔:多重空行
            'rep_no'      => '/【\s*文獻號\s*】(.+)\n/u',    // 文獻號
            'periodical'  => '/【原文出處】(.+)\n/u',        // 原文出處
            'issue'       => '/【原刊期號】(.+)\n/u',        // 原刊期號
            'class_no'    => '/【\s*分\s*類\s*號】(.+)\n/u', // 分類號
            'reprint_issue'=> '/【復印期號】(.+)\n/u',       // 復印期號
            'title'       => '/【\s*標\s*題\s*】(.+)\n【/us', // 標題 (貪婪匹配到下一個【)
            'author'      => '/【\s*作\s*者\s*】(.+)\n/u',   // 作者
            'body'        => '/(.+【\s*正\s*文\s*】)(.+)$/us', // 正文 (保留一個【做結束標記)
            'abstract'    => '/【內容提要】(.+)【/us',       // 內容提要
        ];
    }

    public function processUpload(array $file, array $post): array
    {
        // 1. 驗證上傳
        $this->validateUpload($file);

        // 2. 讀取檔案內容 (大檔案建議流式處理,此處簡化為一次讀入)
        $content = file_get_contents($file['tmp_name']);
        if ($content === false) throw new \RuntimeException('檔案讀取失敗');

        // 3. 分割文章區塊
        $blocks = preg_split($this->patterns['split'], $content);
        $total = count($blocks);

        // 4. 準備索引檔
        $indexTitle = ($post['index_title'] ?? '人大復印資料') . '_' . date('YmdHis');
        $indexFile = 'index_' . date('YmdHis') . '.html';
        $indexPath = $this->config['paths']['index_dir'] . $indexFile;
        $fp = \App\Lib\IndexBuilder::createHandle($indexPath, $indexTitle, $post['index_describe'] ?? '');

        // 5. 批次處理每篇文章
        $inserted = 0;
        $duplicates = 0;
        $errors = [];
        $catalog = (int)($post['catalog'] ?? 0);

        // 預先準備 SQL
        $stmtInsert = $this->pdo->prepare(
            "INSERT INTO article (resource_id, origin_periodical, date_pub, article_author, article_title, 
             article_data, article_type, article_size, date_reprinted, catalog)
             VALUES (:rid, :periodical, :date_pub, :author, :title, :body, :type, :size, :date_rep, :catalog)"
        );
        $stmtCheck = $this->pdo->prepare(
            "SELECT article_id, article_title, article_author, article_type 
             FROM article WHERE resource_id = :rid"
        );

        foreach ($blocks as $i => $block) {
            try {
                $data = $this->extractArticleData($block);
                
                // 必填欄位檢查
                if (empty($data['title']) || empty($data['resource_id'])) {
                    $errors[] = "第 " . ($i+1) . " 篇:標題或文獻號為空,略過";
                    continue;
                }

                // 日期標準化
                $data['date_pub'] = $this->normalizeDate($data['date_pub_raw']);
                $data['date_reprinted'] = $this->normalizeDate($data['reprint_issue_raw'], true);

                // 內容組裝:提要 + 正文
                $fullBody = ($data['abstract'] ? $data['abstract'] . "\r\n" : '') . $data['body'];
                $data['body'] = $fullBody;
                $data['size'] = strlen($fullBody);

                // 6. 去重檢查
                $stmtCheck->execute([':rid' => $data['resource_id']]);
                $isDuplicate = false;
                while ($exist = $stmtCheck->fetch()) {
                    // 同資源編號 + 同分類 + 同作者 視為重複
                    if ($exist['article_type'] === $data['type'] && $exist['article_author'] === $data['author']) {
                        $isDuplicate = true;
                        $errors[] = "第 " . ($i+1) . " 篇:重複資料 (ID:{$exist['article_id']} {$exist['article_title']}),略過";
                        break;
                    }
                }

                if ($isDuplicate) {
                    $duplicates++;
                    continue;
                }

                // 7. 寫入資料庫
                $stmtInsert->execute([
                    ':rid'        => $data['resource_id'],
                    ':periodical' => $data['periodical'],
                    ':date_pub'   => $data['date_pub'],
                    ':author'     => $data['author'],
                    ':title'      => $data['title'],
                    ':body'       => $data['body'],
                    ':type'       => $data['type'],
                    ':size'       => $data['size'],
                    ':date_rep'   => $data['date_reprinted'],
                    ':catalog'    => $catalog,
                ]);
                $newId = $this->pdo->lastInsertId();
                $inserted++;

                // 8. 寫入索引檔
                $fp->fwrite("<tr><td><a href='read.php?id={$newId}'>" . htmlspecialchars($data['title']) . "</a></td>");
                $fp->fwrite("<td>" . htmlspecialchars($data['author']) . "</td></tr>\n");

            } catch (\Throwable $e) {
                $errors[] = "第 " . ($i+1) . " 篇處理失敗: " . $e->getMessage();
            }
        }

        // 9. 收尾
        \App\Lib\IndexBuilder::closeHandle($fp);
        
        // 更新總索引
        $totalIndex = $this->config['paths']['index_dir'] . 'master_index.html';
        $masterFp = new SplFileObject($totalIndex, 'a');
        $masterFp->fwrite("<a href='{$indexFile}'>" . htmlspecialchars($indexTitle) . "</a><br>\n");

        return [
            'inserted'   => $inserted,
            'duplicates' => $duplicates,
            'total'      => $total,
            'index_file' => $indexFile,
            'errors'     => $errors,
        ];
    }

    // 正則提取單篇文章資料
    private function extractArticleData(string $block): array
    {
        $data = [
            'resource_id' => '', 'periodical' => '', 'date_pub_raw' => '',
            'type' => '', 'reprint_issue_raw' => '', 'title' => '',
            'author' => '', 'body' => '', 'abstract' => '',
        ];

        foreach ($this->patterns as $key => $pattern) {
            if ($key === 'split') continue;
            if (preg_match($pattern, $block, $matches)) {
                $val = trim($matches[1]);
                // 移除註腳 (如 (注...))
                $val = preg_replace('/(注.*?)$/s', '', $val);
                $val = preg_replace('/(注.*?)\n\s*/s', '', $val);
                $data[$this->mapKey($key)] = $val;
            }
        }
        return $data;
    }

    private function mapKey(string $key): string
    {
        return match($key) {
            'rep_no' => 'resource_id',
            'periodical' => 'periodical',
            'issue' => 'date_pub_raw',
            'class_no' => 'type',
            'reprint_issue' => 'reprint_issue_raw',
            'title' => 'title',
            'author' => 'author',
            'body' => 'body',
            'abstract' => 'abstract',
            default => $key,
        };
    }

    // 日期標準化:YYYYMMDD 或 YYYYMM → YYYY-MM-DD HH:MM:SS
    private function normalizeDate(string $raw, bool $isReprint = false): string
    {
        $raw = preg_replace('/\D/', '', $raw); // 只保留數字
        if (strlen($raw) >= 8) {
            return substr($raw, 0, 4) . '-' . substr($raw, 4, 2) . '-' . substr($raw, 6, 2) . ' 00:00:00';
        } elseif (strlen($raw) >= 6) {
            return substr($raw, 0, 4) . '-' . substr($raw, 4, 2) . '-01 00:00:00';
        }
        return $isReprint ? date('Y-m-d H:i:s') : '0000-00-00 00:00:00';
    }

    private function validateUpload(array $file): void
    {
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new \RuntimeException('上傳失敗,錯誤代碼: ' . $file['error']);
        }
        if ($file['size'] > $this->config['limits']['max_file_size']) {
            throw new \RuntimeException('檔案過大,限制 ' . ($this->config['limits']['max_file_size'] / 1024 / 1024) . 'MB');
        }
        // 可加入 MIME 型別檢查
    }
}

關鍵技術亮點

1. 正則表達式優化

  • 預編譯: 建構子中 compilePatterns() 一次編譯,迴圈複用。
  • 修飾符運用: u (UTF-8)、s (單行模式 . 匹配換行)、U (非貪婪) 精準控制匹配行為。
  • 邊界處理: 標題匹配 /【\s*標\s*題\s*】(.+)\n【/us 利用下一個 【 作為結束邊界,避免內文包含「【」字符導致截斷 (原始碼註釋提到的 d0a1bee4 編碼衝突問題)。

2. 去重邏輯強化

原始碼僅檢查 resource_id,導致同一編號不同文章誤判重複。新邏輯:資源編號 + 分類號 + 作者 三欄位聯合判斷,並透過 UNIQUE KEY uk_resource_type (resource_id, article_type) 在資料庫層面輔助防禦。

3. 記憶體與效能考量

  • 大檔案: 目前 file_get_contents 一次讀入,超過 50MB 建議改用 SplFileObject 逐行流式處理,或 fread 分塊緩衝。
  • 批次提交: 目前逐筆 execute,大量資料建議累積 100-500 筆後 executeBatch (需 PDO 驅動支援) 或使用 LOAD DATA INFILE。
  • 索引檔: 使用 SplFileObject 寫入,避免手動 fopen/fwrite/fclose 洩漏。

4. 日期標準化健壯性

normalizeDate() 先移除非數字,再根據長度判斷精度 (年月日/年月/無),避免原始碼 substr($date_temp,6,2) 直接取位導致的越界或格式錯誤。

上傳表單範本:templates/import_form.php

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>人大復印資料匯入</title></head>
<body>
<h1>人大復印報刊資料匯入系統</h1>
<form method="post" enctype="multipart/form-data" action="import.php">
    <div>
        <label>資料檔: <input type="file" name="upload_file" required></label>
    </div>
    <div>
        <label>索引檔名: <input type="text" name="index_file" placeholder="自動生成"></label>
    </div>
    <div>
        <label>索引標題: <input type="text" name="index_title" value="人大復印資料"></label>
    </div>
    <div>
        <label>查詢說明:<br>
        <textarea name="index_describe" rows="5" cols="60">人大復印資料(1985-1989)任意詞查詢</textarea>
        </label>
    </div>
    <div>
        <label>文章分類:
        <select name="catalog">
            <option value="11">語言理論</option>
            <option value="12">語法學</option>
            <option value="13">語義學</option>
            <option value="14">語用學</option>
            <option value="15">修辭學</option>
            <option value="16">古代漢語</option>
            <option value="21">歷史學</option>
            <option value="31">中國哲學</option>
            <option value="41">其他</option>
        </select>
        </label>
    </div>
    <button type="submit">開始匯入</button>
</form>
</body></html>

小結

本文完整實作了人大復印資料的現代化匯入管線:從上傳驗證、正則解析、資料清洗、日期標準化、智慧去重、參數化批次寫入、索引同步生成,到結果回報。修正了原始碼的編碼假設、SQL 注入風險、去重邏輯缺陷、記憶體管理、錯誤處理缺失等核心問題。此架構具備良好擴充性,可進一步整合佇列非同步處理、進度條即時回報、解析規則視覺化配置等企業級功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved