本文目录导读:

我来为你详细讲解PHP爬虫数据清洗的完整方案,包括常用技术和最佳实践。
基础数据清洗框架
<?php
class DataCleaner {
private $data;
private $errors = [];
public function __construct($data) {
$this->data = $data;
}
// 主清洗流程
public function clean() {
$this->removeDuplicate();
$this->removeEmpty();
$this->formatText();
$this->formatDate();
$this->formatNumber();
$this->filterSpecialChars();
return [
'data' => $this->data,
'errors' => $this->errors
];
}
// 去重
private function removeDuplicate() {
$this->data = array_unique($this->data, SORT_REGULAR);
}
// 移空值
private function removeEmpty() {
foreach ($this->data as $key => $value) {
if (empty($value) && $value !== '0') {
unset($this->data[$key]);
}
}
}
// 格式化文本
private function formatText() {
foreach ($this->data as $key => $value) {
if (is_string($value)) {
// 去除多余空格
$this->data[$key] = trim(preg_replace('/\s+/', ' ', $value));
// 去除HTML标签
$this->data[$key] = strip_tags($this->data[$key]);
// 转换特殊字符
$this->data[$key] = htmlspecialchars($this->data[$key]);
}
}
}
// 格式化日期
private function formatDate() {
foreach ($this->data as $key => $value) {
if (is_string($value) && $this->isDate($value)) {
$date = date_create($value);
if ($date) {
$this->data[$key] = $date->format('Y-m-d H:i:s');
}
}
}
}
// 格式化数字
private function formatNumber() {
foreach ($this->data as $key => $value) {
if (is_numeric($value)) {
$this->data[$key] = floatval($value);
}
}
}
// 过滤特殊字符
private function filterSpecialChars() {
$pattern = '/[^\w\s\x{4e00}-\x{9fa5},。!?、;:""''()《》@#$%^&*()-+=,.!?;:\/\\|]/u';
foreach ($this->data as $key => $value) {
if (is_string($value)) {
$this->data[$key] = preg_replace($pattern, '', $value);
}
}
}
// 判断是否为日期
private function isDate($value) {
try {
new DateTime($value);
return true;
} catch (Exception $e) {
return false;
}
}
}
高级清洗工具类
<?php
class AdvancedCleaner {
// 提取图片URL
public function extractImages($html) {
$pattern = '/<img[^>]+src=["\']([^"\']+)["\']/i';
preg_match_all($pattern, $html, $matches);
return $matches[1] ?? [];
}
// 提取链接
public function extractLinks($html) {
$pattern = '/<a[^>]+href=["\']([^"\']+)["\']/i';
preg_match_all($pattern, $html, $matches);
return $matches[1] ?? [];
}
// 清洗HTML
public function cleanHtml($html, $allowedTags = []) {
if (empty($allowedTags)) {
$allowedTags = '<p><br><b><strong><i><em><ul><li><ol><h1><h2><h3><h4><h5>';
}
return strip_tags($html, $allowedTags);
}
// 格式化URL
public function normalizeUrl($url, $baseUrl = '') {
if (filter_var($url, FILTER_VALIDATE_URL)) {
return $url;
}
if (strpos($url, 'http') === 0) {
return $url;
}
if (!empty($baseUrl)) {
$base = parse_url($baseUrl);
$scheme = $base['scheme'] ?? 'http';
$host = $base['host'] ?? '';
$path = $base['path'] ?? '';
if (strpos($url, '/') === 0) {
return "{$scheme}://{$host}{$url}";
}
return "{$scheme}://{$host}" . dirname($path) . "/{$url}";
}
return $url;
}
// 清洗JSON数据
public function cleanJson($jsonData) {
$data = json_decode($jsonData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// 尝试修复JSON
$data = $this->repairJson($jsonData);
}
return $data;
}
// 修复损坏的JSON
private function repairJson($json) {
// 添加缺失的引号
$json = preg_replace('/([{,])(\s*)([A-Za-z0-9_]+)\s*:/', '$1"$3":', $json);
// 确保是有效JSON
return json_decode($json, true);
}
// 清洗CSV数据
public function cleanCsv($csvContent) {
$rows = str_getcsv($csvContent, "\n");
$cleanData = [];
foreach ($rows as $row) {
$fields = str_getcsv($row, ',');
$cleanFields = array_map('trim', $fields);
$cleanData[] = $cleanFields;
}
return $cleanData;
}
// 格式转换
public function convertEncoding($data, $from = 'GB2312', $to = 'UTF-8') {
if (is_array($data)) {
return array_map(function($item) use ($from, $to) {
return $this->convertEncoding($item, $from, $to);
}, $data);
}
if (is_string($data)) {
$converted = @iconv($from, $to . '//IGNORE', $data);
if ($converted !== false) {
return $converted;
}
// iconv失败时使用mb_convert_encoding
if (function_exists('mb_convert_encoding')) {
return mb_convert_encoding($data, $to, $from);
}
}
return $data;
}
}
爬虫数据清洗实战示例
<?php
// 爬虫数据清洗完整示例
class CrawlerDataProcessor {
private $rawData;
private $cleaner;
private $advancedCleaner;
public function __construct($rawData) {
$this->rawData = $rawData;
$this->cleaner = new DataCleaner($rawData);
$this->advancedCleaner = new AdvancedCleaner();
}
public function process() {
$processed = [];
foreach ($this->rawData as $item) {
$cleanItem = [];
// 处理标题
if (isset($item['title'])) {
$cleanItem['title'] = $this->cleanText($item['title']);
}
// 处理内容
if (isset($item['content'])) {
$cleanItem['content'] = $this->cleanContent($item['content']);
}
// 处理URL
if (isset($item['url'])) {
$cleanItem['url'] = $this->cleanUrl($item['url']);
}
// 处理图片
if (isset($item['images']) && is_array($item['images'])) {
$cleanItem['images'] = array_map(function($img) {
return $this->cleanUrl($img);
}, $item['images']);
}
// 处理日期
if (isset($item['date'])) {
$cleanItem['date'] = $this->cleanDate($item['date']);
}
// 处理价格
if (isset($item['price'])) {
$cleanItem['price'] = $this->cleanPrice($item['price']);
}
$processed[] = $cleanItem;
}
// 最终清洗
$result = $this->cleaner->clean();
return [
'data' => $processed,
'count' => count($processed),
'errors' => $result['errors']
];
}
// 清洗文本
private function cleanText($text) {
// 移除HTML标签
$text = strip_tags($text);
// 移除多余空格
$text = preg_replace('/\s+/', ' ', $text);
// 移除控制字符
$text = preg_replace('/[\x00-\x1F\x7F]/', '', $text);
// 转换编码
$text = mb_convert_encoding($text, 'UTF-8', 'auto');
return trim($text);
}
// 清洗内容
private function cleanContent($content) {
// 保留基本HTML标签
$allowedTags = '<p><br><b><strong><i><em><ul><li><ol><h1><h2><h3><h4><h5>';
$content = strip_tags($content, $allowedTags);
// 移除script和style
$content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
$content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
// 移除注释
$content = preg_replace('/<!--.*?-->/s', '', $content);
return $content;
}
// 清洗URL
private function cleanUrl($url) {
$url = html_entity_decode($url);
$url = trim($url);
// 检查URL合法性
if (!filter_var($url, FILTER_VALIDATE_URL)) {
// 尝试修复相对路径
$url = $this->advancedCleaner->normalizeUrl($url);
}
// 移除JavaScript链接
if (preg_match('/^javascript:/i', $url)) {
return '';
}
return $url;
}
// 清洗日期
private function cleanDate($date) {
// 处理各种日期格式
$patterns = [
'/(\d{4})年(\d{1,2})月(\d{1,2})日/',
'/(\d{4})-(\d{1,2})-(\d{1,2})/',
'/(\d{4})\/(\d{1,2})\/(\d{1,2})/'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $date, $matches)) {
return sprintf("%04d-%02d-%02d", $matches[1], $matches[2], $matches[3]);
}
}
// 尝试直接解析
$timestamp = strtotime($date);
if ($timestamp !== false) {
return date('Y-m-d', $timestamp);
}
return null;
}
// 清洗价格
private function cleanPrice($price) {
// 移除货币符号和空格
$price = preg_replace('/[^\d.]/', '', $price);
return floatval($price);
}
}
数据验证和异常处理
<?php
class DataValidator {
public function validate($data, $rules) {
$errors = [];
foreach ($rules as $field => $fieldRules) {
$value = $data[$field] ?? null;
foreach ($fieldRules as $rule) {
$result = $this->validateRule($field, $value, $rule);
if ($result !== true) {
$errors[$field][] = $result;
}
}
}
return $errors;
}
private function validateRule($field, $value, $rule) {
if (is_string($rule)) {
return $this->applyRule($field, $value, $rule);
}
if (is_array($rule)) {
$type = $rule[0] ?? '';
$params = $rule[1] ?? null;
return $this->applyRule($field, $value, $type, $params);
}
return true;
}
private function applyRule($field, $value, $rule, $params = null) {
switch ($rule) {
case 'required':
return !empty($value) ? true : "$field is required";
case 'email':
return filter_var($value, FILTER_VALIDATE_EMAIL) ? true : "$field is not a valid email";
case 'url':
return filter_var($value, FILTER_VALIDATE_URL) ? true : "$field is not a valid URL";
case 'numeric':
return is_numeric($value) ? true : "$field must be numeric";
case 'min_length':
return strlen($value) >= $params ? true : "$field must be at least $params characters";
case 'max_length':
return strlen($value) <= $params ? true : "$field must be no more than $params characters";
case 'regex':
return preg_match($params, $value) ? true : "$field doesn't match the pattern";
default:
return true;
}
}
}
缓存和存储优化
<?php
class DataStorage {
private $connection;
public function __construct($host, $user, $pass, $dbname) {
$this->connection = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
}
// 批量插入
public function batchInsert($table, $data) {
$sql = "INSERT INTO $table (";
$columns = array_keys($data[0]);
$sql .= implode(', ', $columns) . ") VALUES ";
$values = [];
foreach ($data as $item) {
$placeholders = array_fill(0, count($columns), '?');
$values = array_merge($values, array_values($item));
$sql .= "(" . implode(', ', $placeholders) . "), ";
}
$sql = rtrim($sql, ', ');
$stmt = $this->connection->prepare($sql);
$stmt->execute($values);
return $stmt->rowCount();
}
// 增量更新
public function upsert($table, $data, $uniqueFields) {
$columns = array_keys($data);
$sql = "INSERT INTO $table (" . implode(', ', $columns) . ") VALUES (";
$placeholders = array_fill(0, count($columns), '?');
$sql .= implode(', ', $placeholders) . ") ";
// 添加ON DUPLICATE KEY UPDATE
$updates = [];
foreach ($columns as $column) {
if (!in_array($column, $uniqueFields)) {
$updates[] = "$column = VALUES($column)";
}
}
$sql .= "ON DUPLICATE KEY UPDATE " . implode(', ', $updates);
$stmt = $this->connection->prepare($sql);
$stmt->execute(array_values($data));
return $stmt->rowCount();
}
}
完整使用示例
<?php
// 使用示例
$rawData = [
[
'title' => ' Apple iPhone 15 Pro ',
'content' => '<p>最新款<i>智能手机</i></p><script>alert("xss")</script>',
'url' => '/products/iphone-15-pro',
'price' => '¥9,999.00',
'date' => '2024年1月15日',
'images' => ['http://example.com/img1.jpg', '/img2.jpg']
],
// ... 更多数据
];
// 创建处理器
$processor = new CrawlerDataProcessor($rawData);
// 处理数据
$result = $processor->process();
// 验证数据
$validator = new DataValidator();
$validationErrors = $validator->validate($result['data'], [ => ['required', ['min_length', 10]],
'url' => ['url'],
'price' => ['numeric']
]);
// 输出结果
echo "处理数据数量: " . $result['count'] . "\n";
if (!empty($validationErrors)) {
echo "验证错误:\n";
print_r($validationErrors);
} else {
echo "数据验证通过!\n";
}
// 存储数据
$storage = new DataStorage('localhost', 'user', 'password', 'crawler');
foreach (array_chunk($result['data'], 100) as $chunk) { // 分批插入
$storage->batchInsert('products', $chunk);
}
性能优化建议
- 使用批量处理:分批处理数据,避免内存溢出
- 过滤无效数据:在早期阶段就过滤掉无用数据
- 使用缓存:对常用数据使用内存缓存
- 并行处理:使用多进程处理大量数据
- 合理的正则表达式:避免过度复杂的正则表达式
这个完整的PHP爬虫数据清洗方案覆盖了数据清洗的各个方面,包括基础清洗、高级处理、验证和存储,你可以根据实际需求调整和扩展这些功能。