本文目录导读:

针对PHP项目的论文查重系统,要实现批量比对文档内容,核心在于解决两个问题:
- 文本解析:从不同格式的文档(如PDF、Word、纯文本)中提取纯文本。
- 相似度算法:对提取出的文本进行高效比对。
以下是一个分阶段的实现方案,侧重于独立查重(论文库本地化)或互相比对(多文档两两比对)。
第一阶段:环境准备与文档解析
PHP处理文档格式(尤其是PDF和DOCX)比较麻烦,建议使用以下库:
安装依赖(Composer)
composer require phpoffice/phpword # 处理 .docx composer require smalot/pdfparser # 处理 .pdf
对于 .doc 格式,建议用户先将文档转换为 .docx 或使用服务器上的 LibreOffice 进行转换(较复杂,不推荐纯PHP)。
文本提取核心类
use PhpOffice\PhpWord\IOFactory;
use Smalot\PdfParser\Parser;
class TextExtractor {
public static function extract($filePath) : string {
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
switch ($ext) {
case 'docx':
$phpWord = IOFactory::load($filePath);
$text = '';
foreach ($phpWord->getSections() as $section) {
foreach ($section->getElements() as $element) {
if ($element instanceof \PhpOffice\PhpWord\Element\TextRun) {
foreach ($element->getElements() as $piece) {
$text .= $piece->getText();
}
} elseif ($element instanceof \PhpOffice\PhpWord\Element\Text) {
$text .= $element->getText();
}
}
}
return $text;
case 'pdf':
$parser = new Parser();
$pdf = $parser->parseFile($filePath);
return $pdf->getText();
case 'txt':
return file_get_contents($filePath);
default:
throw new \InvalidArgumentException("不支持的文件格式: $ext");
}
}
}
第二阶段:文本预处理与比对算法(核心)
批量比对的核心在于分词+哈希指纹,如果全文逐字比对,性能会非常差。
选择方法(推荐 SimHash + 汉明距离)
- 原理:将长文本压缩为一个64位或128位的哈希值(指纹),通过计算两个哈希之间不同位的数量(汉明距离)来估算相似度。
- 优势:速度快,适合海量文档。
- 劣势:对短文本(< 几百字)不敏感。
实现 SimHash(纯PHP版)
class SimHash {
private int $bits = 64;
public function hash(string $text): string {
// 1. 分词(使用简单的正则或外部分词库,如 jieba-php)
$words = $this->tokenize($text);
// 初始化特征向量
$v = array_fill(0, $this->bits, 0);
foreach ($words as $word => $weight) {
$hash = $this->simpleHash($word);
for ($i = 0; $i < $this->bits; $i++) {
$bit = ($hash >> ($this->bits - 1 - $i)) & 1;
if ($bit === 1) {
$v[$i] += $weight;
} else {
$v[$i] -= $weight;
}
}
}
$fingerprint = 0;
for ($i = 0; $i < $this->bits; $i++) {
if ($v[$i] > 0) {
$fingerprint |= (1 << ($this->bits - 1 - $i));
}
}
return $fingerprint;
}
// 简易分词(实际生产环境请使用 jieba-php)
private function tokenize(string $text): array {
// 去掉标点符号,按空格/逗号分割(中文效果较差)
// 强烈建议:这里接入 jieba-php 进行中文分词
$text = preg_replace('/\pP/u', ' ', $text);
$words = preg_split('/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY);
// 返回词频作为权重
return array_count_values($words);
}
// 简易哈希(32位 FNV-1a)
private function simpleHash(string $str): int {
$hash = 0x811c9dc5;
$prime = 0x01000193;
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$hash ^= ord($str[$i]);
$hash *= $prime;
}
// 返回64位
return ($hash & 0xFFFFFFFF) << 32 | ($hash & 0xFFFFFFFF);
}
// 计算汉明距离
public function hammingDistance(int $hash1, int $hash2): int {
$xor = $hash1 ^ $hash2;
$dist = 0;
while ($xor) {
$dist++;
$xor &= ($xor - 1);
}
return $dist;
}
// 将距离转换为百分比
public static function similarityPercent(int $distance, int $totalBits = 64): float {
$threshold = $totalBits * 0.15; // 15%不同算完全相似(经典阈值)
if ($distance <= 3) return 100.0; // 极相似
if ($distance > $threshold) return 0.0; // 不相似
// 线性映射(0~15%差异对应100%->0%相似度)
return max(0, 100 - ($distance / $threshold * 100));
}
}
第三阶段:批量比对逻辑
假设我们有一个文档列表($docFiles),需要两两比对。
离线计算指纹(缓存)
$fingerprints = [];
foreach ($docFiles as $file) {
$text = TextExtractor::extract($file);
$simHash = new SimHash();
$fingerprints[$file] = $simHash->hash($text);
}
生成比对矩阵
$results = [];
$simHashObj = new SimHash();
$files = array_keys($fingerprints);
for ($i = 0; $i < count($files); $i++) {
for ($j = $i + 1; $j < count($files); $j++) {
$dist = $simHashObj->hammingDistance($fingerprints[$files[$i]], $fingerprints[$files[$j]]);
$similarity = SimHash::similarityPercent($dist);
$results[] = [
'file1' => basename($files[$i]),
'file2' => basename($files[$j]),
'similarity' => round($similarity, 2) . '%',
'hamming_distance' => $dist
];
}
}
输出结果
可以以表格形式输出到浏览器、CSV或JSON。
第四阶段:进阶优化(瓶颈突破)
| 问题 | 解决方案 |
|---|---|
| PHP处理大文件慢 | 使用 Swoole 或 ReactPHP 实现异步解析;或使用 exec() 调用 Python 脚本(推荐结合 pypdf2 + jieba)。 |
| 中文分词不准 | 在 tokenize() 中集成 jieba-php(fukuball/jieba-php)。 |
| 指纹比较O(n²) | 对指纹建立 倒排索引:将64位指纹分成4个16位块,每个块作为key,相同块hash的文档大概率相似,只在这些块中比较(分桶优化)。 |
| 需要比对数据库 | 将论文库的SimHash存入Redis(Sorted Set),利用 SIMHASH 命令快速查找近似文档(Redis 7+支持)。 |
完整调用示例
$files = [
'doc1.docx',
'thesis.pdf',
'paper.txt'
];
$results = batchCompare($files);
function batchCompare(array $files): array {
// 1. 提取并计算指纹
$fingerprints = [];
foreach ($files as $f) {
try {
$text = TextExtractor::extract($f);
$simHash = new SimHash();
$fingerprints[$f] = $simHash->hash($text);
} catch (Exception $e) {
echo "处理 {$f} 失败: " . $e->getMessage() . "\n";
}
}
// 2. 两两比对
$results = [];
$list = array_keys($fingerprints);
$obj = new SimHash();
for ($i = 0; $i < count($list); $i++) {
for ($j = $i + 1; $j < count($list); $j++) {
$d = $obj->hammingDistance($fingerprints[$list[$i]], $fingerprints[$list[$j]]);
$s = SimHash::similarityPercent($d);
$results[] = [
basename($list[$i]) . ' vs ' . basename($list[$j]) => $s . '%'
];
}
}
return $results;
}
print_r($results);
最终建议
- 不要依赖纯PHP处理50页以上的PDF:建议使用
pdftotext(Linux)或 Python 库作为后端服务,PHP通过HTTP调用。 - 对于1000+文档的批量比对:使用 MinHash(布隆过滤器的变种) + LSH(局部敏感哈希),将复杂度从O(n²)降到近似线性。
- 数据库方案:将指纹存入 MySQL(
BIGINT UNSIGNED列),SELECT * FROM papers WHERE hash BIT_XOR ? HAVING BIT_COUNT(...) < 10(但性能不佳),推荐使用 Elasticsearch 的more_like_this查询。