Day 9: 数据存储与清洗
学习目标
- 掌握常见数据存储方案
- 学会使用 MySQL 和 MongoDB 存储数据
- 掌握数据清洗技术
- 了解数据验证方法
技术原理
9.1 数据存储方案
文件存储:
- CSV: 简单表格数据
- JSON: 结构化数据
- XML: 标记语言数据
数据库存储:
- MySQL: 关系型数据库
- MongoDB: 文档数据库
- Redis: 缓存数据库
9.2 数据清洗
数据清洗是将原始数据转换为可用格式的过程:
常见清洗任务:
- 去除空值和无效数据
- 统一数据格式
- 处理异常值
- 数据类型转换
9.3 数据验证
数据验证确保数据符合预期格式:
验证方法:
- 类型检查
- 范围验证
- 格式验证
- 完整性检查
案例
案例1:CSV 文件存储
python
import csv
from typing import List, Dict
class CSVStorage:
"""CSV 存储器"""
def __init__(self, filename, encoding='utf-8-sig'):
self.filename = filename
self.encoding = encoding
def save(self, data: List[Dict]):
"""保存数据"""
if not data:
return
# 获取字段名
fields = data[0].keys()
with open(self.filename, 'w', newline='', encoding=self.encoding) as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(data)
def append(self, item: Dict):
"""追加数据"""
with open(self.filename, 'a', newline='', encoding=self.encoding) as f:
writer = csv.DictWriter(f, fieldnames=item.keys())
writer.writerow(item)
def load(self) -> List[Dict]:
"""加载数据"""
data = []
with open(self.filename, 'r', encoding=self.encoding) as f:
reader = csv.DictReader(f)
for row in reader:
data.append(row)
return data
# 使用示例
storage = CSVStorage('products.csv')
# 保存数据
products = [
{'name': '商品1', 'price': '99.00', 'stock': '100'},
{'name': '商品2', 'price': '199.00', 'stock': '50'},
]
storage.save(products)
# 加载数据
loaded = storage.load()
print(f'加载 {len(loaded)} 条数据')案例2:JSON 文件存储
python
import json
from typing import List, Dict
class JSONStorage:
"""JSON 存储器"""
def __init__(self, filename, encoding='utf-8'):
self.filename = filename
self.encoding = encoding
def save(self, data):
"""保存数据"""
with open(self.filename, 'w', encoding=self.encoding) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load(self):
"""加载数据"""
with open(self.filename, 'r', encoding=self.encoding) as f:
return json.load(f)
def append(self, item):
"""追加数据"""
try:
data = self.load()
except (FileNotFoundError, json.JSONDecodeError):
data = []
data.append(item)
self.save(data)
# 使用示例
storage = JSONStorage('data.json')
# 保存数据
data = {
'name': '爬虫项目',
'version': '1.0',
'items': [{'id': 1, 'name': 'item1'}]
}
storage.save(data)
# 加载数据
loaded = storage.load()
print(f'项目名: {loaded["name"]}')应用场景
1. 结构化数据存储
- 商品信息
- 用户数据
- 订单数据
2. 非结构化数据存储
- 网页内容
- 图片文件
- 日志数据
3. 缓存和队列
- URL 去重
- 任务队列
- 会话管理
代码案例
案例3:MySQL 存储
python
import pymysql
from typing import List, Dict
class MySQLStorage:
"""MySQL 存储器"""
def __init__(self, host='localhost', port=3306, user='root',
password='', database='crawler'):
self.connection = pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset='utf8mb4'
)
self.cursor = self.connection.cursor()
def create_table(self, table_name, columns):
"""创建表"""
cols = ', '.join([f'{col} {dtype}' for col, dtype in columns.items()])
sql = f'CREATE TABLE IF NOT EXISTS {table_name} ({cols})'
self.cursor.execute(sql)
self.connection.commit()
def insert(self, table_name, data: Dict):
"""插入数据"""
fields = ', '.join(data.keys())
placeholders = ', '.join(['%s'] * len(data))
sql = f'INSERT INTO {table_name} ({fields}) VALUES ({placeholders})'
self.cursor.execute(sql, list(data.values()))
self.connection.commit()
return self.cursor.lastrowid
def insert_many(self, table_name, data_list: List[Dict]):
"""批量插入"""
if not data_list:
return
fields = ', '.join(data_list[0].keys())
placeholders = ', '.join(['%s'] * len(data_list[0]))
sql = f'INSERT INTO {table_name} ({fields}) VALUES ({placeholders})'
values = [list(item.values()) for item in data_list]
self.cursor.executemany(sql, values)
self.connection.commit()
def query(self, sql, params=None):
"""查询数据"""
self.cursor.execute(sql, params)
return self.cursor.fetchall()
def close(self):
"""关闭连接"""
self.cursor.close()
self.connection.close()
# 使用示例
storage = MySQLStorage(
host='localhost',
user='root',
password='password',
database='crawler'
)
# 创建表
storage.create_table('products', {
'id': 'INT AUTO_INCREMENT PRIMARY KEY',
'name': 'VARCHAR(255)',
'price': 'DECIMAL(10,2)',
'created_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
})
# 插入数据
product_id = storage.insert('products', {
'name': '商品1',
'price': 99.00
})
print(f'插入成功,ID: {product_id}')
# 查询数据
results = storage.query('SELECT * FROM products')
for row in results:
print(row)
storage.close()案例4:MongoDB 存储
python
from pymongo import MongoClient
from typing import List, Dict
class MongoDBStorage:
"""MongoDB 存储器"""
def __init__(self, host='localhost', port=27017, database='crawler'):
self.client = MongoClient(host, port)
self.db = self.client[database]
def get_collection(self, collection_name):
"""获取集合"""
return self.db[collection_name]
def insert(self, collection_name, data: Dict):
"""插入数据"""
collection = self.get_collection(collection_name)
result = collection.insert_one(data)
return result.inserted_id
def insert_many(self, collection_name, data_list: List[Dict]):
"""批量插入"""
collection = self.get_collection(collection_name)
result = collection.insert_many(data_list)
return result.inserted_ids
def find(self, collection_name, query=None, projection=None):
"""查询数据"""
collection = self.get_collection(collection_name)
return list(collection.find(query or {}, projection))
def find_one(self, collection_name, query=None):
"""查询单条数据"""
collection = self.get_collection(collection_name)
return collection.find_one(query or {})
def update(self, collection_name, query, update_data):
"""更新数据"""
collection = self.get_collection(collection_name)
result = collection.update_many(query, {'$set': update_data})
return result.modified_count
def delete(self, collection_name, query):
"""删除数据"""
collection = self.get_collection(collection_name)
result = collection.delete_many(query)
return result.deleted_count
def count(self, collection_name, query=None):
"""统计数量"""
collection = self.get_collection(collection_name)
return collection.count_documents(query or {})
def close(self):
"""关闭连接"""
self.client.close()
# 使用示例
storage = MongoDBStorage(database='crawler')
# 插入数据
product = {
'name': '商品1',
'price': 99.00,
'tags': ['热销', '新品'],
'specs': {
'color': '红色',
'size': 'L'
}
}
product_id = storage.insert('products', product)
print(f'插入成功,ID: {product_id}')
# 查询数据
results = storage.find('products', {'price': {'$gt': 50}})
for item in results:
print(item)
storage.close()案例5:数据清洗工具
python
import re
from typing import Any, Dict
class DataCleaner:
"""数据清洗器"""
@staticmethod
def clean_string(value):
"""清洗字符串"""
if not isinstance(value, str):
return str(value)
# 去除首尾空白
value = value.strip()
# 去除多余空格
value = re.sub(r'\s+', ' ', value)
# 去除特殊字符
value = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', value)
return value
@staticmethod
def clean_number(value):
"""清洗数字"""
if isinstance(value, (int, float)):
return value
if isinstance(value, str):
# 去除非数字字符
value = re.sub(r'[^\d.-]', '', value)
try:
if '.' in value:
return float(value)
return int(value)
except ValueError:
return None
return None
@staticmethod
def clean_price(value):
"""清洗价格"""
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
# 去除货币符号
value = re.sub(r'[¥$€£]', '', value)
# 去除千位分隔符
value = value.replace(',', '')
try:
return float(value)
except ValueError:
return None
return None
@staticmethod
def clean_url(url, base_url=''):
"""清洗URL"""
if not url:
return None
url = url.strip()
# 处理相对URL
if url.startswith('//'):
url = 'https:' + url
elif url.startswith('/'):
from urllib.parse import urlparse
parsed = urlparse(base_url)
url = f'{parsed.scheme}://{parsed.netloc}{url}'
return url
@staticmethod
def clean_date(date_str):
"""清洗日期"""
from datetime import datetime
if isinstance(date_str, datetime):
return date_str
if isinstance(date_str, str):
# 尝试多种日期格式
formats = [
'%Y-%m-%d',
'%Y/%m/%d',
'%Y年%m月%d日',
'%Y-%m-%d %H:%M:%S',
'%Y/%m/%d %H:%M:%S',
]
for fmt in formats:
try:
return datetime.strptime(date_str.strip(), fmt)
except ValueError:
continue
return None
def clean_item(self, item: Dict, rules: Dict) -> Dict:
"""清洗数据项"""
cleaned = {}
for key, value in item.items():
if key in rules:
rule = rules[key]
if rule == 'string':
cleaned[key] = self.clean_string(value)
elif rule == 'number':
cleaned[key] = self.clean_number(value)
elif rule == 'price':
cleaned[key] = self.clean_price(value)
elif rule == 'url':
cleaned[key] = self.clean_url(value)
elif rule == 'date':
cleaned[key] = self.clean_date(value)
else:
cleaned[key] = value
else:
cleaned[key] = value
return cleaned
# 使用示例
cleaner = DataCleaner()
# 清洗价格
price = cleaner.clean_price('¥1,234.56')
print(f'清洗后价格: {price}') # 1234.56
# 清洗数据项
item = {
'name': ' 商品名称 ',
'price': '¥99.00',
'stock': '100件',
'url': '/product/123'
}
rules = {
'name': 'string',
'price': 'price',
'stock': 'number',
'url': 'url'
}
cleaned = cleaner.clean_item(item, rules)
print(f'清洗后: {cleaned}')案例6:数据验证器
python
from typing import Any, Dict, List
import re
class DataValidator:
"""数据验证器"""
@staticmethod
def validate_type(value, expected_type):
"""验证类型"""
if expected_type == 'str':
return isinstance(value, str)
elif expected_type == 'int':
return isinstance(value, int)
elif expected_type == 'float':
return isinstance(value, (int, float))
elif expected_type == 'list':
return isinstance(value, list)
elif expected_type == 'dict':
return isinstance(value, dict)
return False
@staticmethod
def validate_range(value, min_val=None, max_val=None):
"""验证范围"""
if not isinstance(value, (int, float)):
return False
if min_val is not None and value < min_val:
return False
if max_val is not None and value > max_val:
return False
return True
@staticmethod
def validate_pattern(value, pattern):
"""验证正则表达式"""
if not isinstance(value, str):
return False
return bool(re.match(pattern, value))
@staticmethod
def validate_email(email):
"""验证邮箱"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
@staticmethod
def validate_phone(phone):
"""验证手机号"""
pattern = r'^1[3-9]\d{9}$'
return bool(re.match(pattern, phone))
def validate_item(self, item: Dict, schema: Dict) -> List[str]:
"""验证数据项"""
errors = []
for field, rules in schema.items():
value = item.get(field)
# 检查必填字段
if rules.get('required') and value is None:
errors.append(f'{field} 是必填字段')
continue
if value is None:
continue
# 验证类型
if 'type' in rules:
if not self.validate_type(value, rules['type']):
errors.append(f'{field} 类型错误,期望 {rules["type"]}')
# 验证范围
if 'min' in rules or 'max' in rules:
if not self.validate_range(value, rules.get('min'), rules.get('max')):
errors.append(f'{field} 超出范围')
# 验证正则
if 'pattern' in rules:
if not self.validate_pattern(value, rules['pattern']):
errors.append(f'{field} 格式不正确')
return errors
# 使用示例
validator = DataValidator()
# 验证数据
schema = {
'name': {'type': 'str', 'required': True},
'price': {'type': 'float', 'required': True, 'min': 0},
'email': {'type': 'str', 'pattern': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'},
}
item = {
'name': '商品1',
'price': 99.00,
'email': 'test@example.com'
}
errors = validator.validate_item(item, schema)
if errors:
print(f'验证失败: {errors}')
else:
print('验证通过')案例7:完整的数据处理管道
python
import json
from typing import List, Dict
class DataPipeline:
"""数据处理管道"""
def __init__(self):
self.cleaner = DataCleaner()
self.validator = DataValidator()
self.storage = None
def set_storage(self, storage):
"""设置存储器"""
self.storage = storage
def process(self, items: List[Dict], clean_rules: Dict,
validation_schema: Dict) -> Dict:
"""处理数据"""
results = {
'total': len(items),
'success': 0,
'failed': 0,
'errors': []
}
valid_items = []
for i, item in enumerate(items):
try:
# 清洗数据
cleaned = self.cleaner.clean_item(item, clean_rules)
# 验证数据
errors = self.validator.validate_item(cleaned, validation_schema)
if errors:
results['failed'] += 1
results['errors'].append({
'index': i,
'item': item,
'errors': errors
})
else:
valid_items.append(cleaned)
results['success'] += 1
except Exception as e:
results['failed'] += 1
results['errors'].append({
'index': i,
'item': item,
'errors': [str(e)]
})
# 存储有效数据
if self.storage and valid_items:
self.storage.save(valid_items)
return results
# 使用示例
pipeline = DataPipeline()
# 设置存储
storage = JSONStorage('cleaned_data.json')
pipeline.set_storage(storage)
# 定义规则
clean_rules = {
'name': 'string',
'price': 'price',
'url': 'url'
}
validation_schema = {
'name': {'type': 'str', 'required': True},
'price': {'type': 'float', 'required': True, 'min': 0},
}
# 处理数据
items = [
{'name': ' 商品1 ', 'price': '¥99.00', 'url': '/product/1'},
{'name': '商品2', 'price': '¥-10.00', 'url': '/product/2'}, # 价格错误
{'name': '商品3', 'price': '¥199.00', 'url': '/product/3'},
]
results = pipeline.process(items, clean_rules, validation_schema)
print(f'处理结果: {results}')课后练习
练习1:实现 Redis 存储器
实现一个支持多种数据类型的 Redis 存储器。
练习2:实现数据去重
实现一个基于 URL 和标题的数据去重功能。
练习3:实现数据导出
实现一个支持导出为 Excel 格式的数据导出器。
常见问题
Q1: 如何选择数据库?
A: 结构化数据用 MySQL,非结构化数据用 MongoDB,缓存和队列用 Redis。
Q2: 如何处理大量数据?
A: 使用批量插入,分页查询,索引优化。
Q3: 如何保证数据质量?
A: 数据清洗、数据验证、异常处理、日志记录。