数据清洗
# 📚 概述
数据清洗是数据处理流程中的关键环节,用于从原始数据中识别和纠正错误、不一致性,以及处理缺失值,最终得到高质量、可用的数据。Hutool 提供了丰富的工具类来简化数据清洗过程,主要包括:
- 数据抽取:使用
HttpUtil、ReUtil等工具从各种数据源获取数据 - 数据转换:使用
DateUtil、NumberUtil、StrUtil等工具转换数据格式 - 数据清洗:使用
CollUtil、StrUtil、Validator等工具处理数据质量问题 - 数据入库:使用
Db模块将清洗后的数据保存到数据库
通过这些工具的组合使用,可以高效地完成从数据采集到最终入库的完整数据清洗流程。
# 🚀 快速开始
# 1. 添加依赖
<!-- 如果使用MySQL数据库,需要添加MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
# 2. 配置数据库
在 src/main/resources 目录下创建 db.setting 文件:
url = jdbc:mysql://localhost:3306/data_cleaning
user = root
pass = 123456
## 可选配置
showSql = true
formatSql = false
showParams = true
sqlLevel = debug
# 📖 核心功能
# 一、数据抽取
数据抽取是数据清洗的第一步,用于从各种数据源获取原始数据。
# 1. 从网页获取数据
// 使用 HttpUtil 获取网页内容
String url = "https://www.example.com/data";
String html = HttpUtil.get(url);
// 使用 ReUtil 提取数据
String title = ReUtil.get("<title>(.*?)</title>", html, 1);
List<String> links = ReUtil.findAll("<a href=\"(.*?)\"", html, 1);
# 2. 从文件获取数据
// 从文本文件读取数据
List<String> lines = FileUtil.readLines("data.txt", CharsetUtil.UTF_8);
// 从 CSV 文件读取数据
CsvReader reader = CsvUtil.getReader();
List<CsvRow> rows = reader.read(FileUtil.file("data.csv"));
# 3. 从 JSON API 获取数据
// 从 API 获取 JSON 数据
String apiUrl = "https://api.example.com/data";
String jsonStr = HttpUtil.get(apiUrl);
// 解析 JSON 数据
JSONObject json = JSONUtil.parseObj(jsonStr);
JSONArray dataArray = json.getJSONArray("data");
# 二、数据转换
数据转换是将原始数据转换为标准化格式的过程,包括日期转换、数字转换、字符串转换等。
# 1. 日期转换
// 自动识别日期格式并转换
String dateStr = "2023-01-01";
Date date = DateUtil.parse(dateStr);
// 自定义日期格式转换
String customDateStr = "2023/01/01 12:00:00";
Date customDate = DateUtil.parse(customDateStr, "yyyy/MM/dd HH:mm:ss");
// 日期格式化
String formattedDate = DateUtil.format(date, "yyyy-MM-dd");
String formattedDateTime = DateUtil.formatDateTime(date);
// 相对时间转换
Date yesterday = DateUtil.yesterday();
Date lastWeek = DateUtil.offsetWeek(new Date(), -1);
Date nextMonth = DateUtil.offsetMonth(new Date(), 1);
// 时间戳转换
long timestamp = System.currentTimeMillis();
Date dateFromTimestamp = DateUtil.date(timestamp);
# 2. 数字转换
// 字符串转数字
String numStr = "123.45";
double num = NumberUtil.parseDouble(numStr);
int intNum = NumberUtil.parseInt("123");
// 数字格式化
String formattedNum = NumberUtil.formatNum(1234567.89, 2); // 1,234,567.89
String formattedPercent = NumberUtil.formatPercent(0.756, 1); // 75.6%
// 数字四舍五入
BigDecimal rounded = NumberUtil.round(123.456, 2); // 123.46
// 数值比较
boolean isGreater = NumberUtil.isGreater(100, 99); // true
boolean isBetween = NumberUtil.isBetween(50, 0, 100); // true
# 3. 字符串转换
// 大小写转换
String upper = StrUtil.upperFirst("hello"); // Hello
String lower = StrUtil.lowerFirst("HELLO"); // hELLO
// 去除空白字符
String cleanStr = StrUtil.cleanBlank(" hello world "); // helloworld
// 去除前后空白
String trimmed = StrUtil.trim(" hello "); // hello
// 替换字符串
String replaced = StrUtil.replace("hello world", "world", "Hutool"); // hello Hutool
// 分割字符串
String[] parts = StrUtil.split("a,b,c", ","); // ["a", "b", "c"]
// 字符串转数组
int[] intArray = Convert.toIntArray("1,2,3,4,5"); // [1, 2, 3, 4, 5]
# 4. JSON 转换
// JSON 字符串转对象
String jsonStr = "{\"name\":\"张三\",\"age\":25}";
JSONObject jsonObj = JSONUtil.parseObj(jsonStr);
// JSON 转 JavaBean
User user = JSONUtil.toBean(jsonStr, User.class);
// JavaBean 转 JSON
String userJson = JSONUtil.toJsonStr(user);
// JSONArray 转 List
JSONArray jsonArray = JSONUtil.parseArray("[1,2,3,4,5]");
List<Integer> list = JSONUtil.toList(jsonArray, Integer.class);
# 三、数据清洗
数据清洗是处理数据质量问题的过程,包括去重、缺失值处理、格式标准化等。
# 1. 数据去重
// 集合去重
List<String> list = CollUtil.newArrayList("a", "b", "a", "c", "b");
List<String> distinctList = CollUtil.distinct(list); // ["a", "b", "c"]
// 根据属性去重
List<User> users = CollUtil.newArrayList(
new User("张三", 25),
new User("李四", 30),
new User("张三", 25)
);
List<User> distinctUsers = CollUtil.distinctByProperty(users, "name");
// 数据库去重
// 查询已存在的数据
List<Entity> existingData = Db.use().findAll("user");
Set<String> existingNames = existingData.stream()
.map(e -> e.getStr("name"))
.collect(Collectors.toSet());
// 过滤重复数据
List<User> newUsers = users.stream()
.filter(user -> !existingNames.contains(user.getName()))
.collect(Collectors.toList());
# 2. 缺失值处理
// 替换空值
String str = StrUtil.emptyToDefault(null, "默认值"); // 默认值
Object value = ObjectUtil.defaultIfNull(null, "默认值"); // 默认值
// 过滤空值
List<String> listWithNull = CollUtil.newArrayList("a", null, "b", "", "c");
List<String> listWithoutNull = CollUtil.removeEmpty(listWithNull); // ["a", "b", "c"]
// 缺失值填充
for (User user : users) {
if (StrUtil.isBlank(user.getName())) {
user.setName("未知");
}
if (user.getAge() == null) {
user.setAge(0);
}
}
# 3. 格式标准化
// 标准化日期格式
List<String> dateStrs = CollUtil.newArrayList("2023-01-01", "2023/01/02", "2023.01.03");
List<String> standardizedDates = dateStrs.stream()
.map(dateStr -> {
Date date = DateUtil.parse(dateStr);
return DateUtil.format(date, "yyyy-MM-dd");
})
.collect(Collectors.toList());
// 标准化手机号格式
String phone = "138 1234 5678";
String standardizedPhone = phone.replaceAll("\\s+", ""); // 13812345678
// 标准化邮箱格式
String email = "USER@EXAMPLE.COM";
String standardizedEmail = email.toLowerCase(); // user@example.com
# 4. 数据验证
// 验证邮箱格式
boolean isEmail = Validator.isEmail("user@example.com"); // true
// 验证手机号格式
boolean isPhone = Validator.isMobile("13812345678"); // true
// 验证日期格式
boolean isDate = Validator.isDate("2023-01-01"); // true
// 验证数字范围
boolean isInRange = Validator.isBetween(50, 0, 100); // true
// 自定义验证规则
boolean isValid = Validator.isMatchRegex("^[a-zA-Z0-9]+$", "abc123"); // true
# 5. 数据过滤
// 按条件过滤集合
List<Integer> numbers = CollUtil.newArrayList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = CollUtil.filter(numbers, number -> number % 2 == 0); // [2, 4]
// 移除不符合条件的数据
CollUtil.removeIf(numbers, number -> number < 3); // [3, 4, 5]
// 分页过滤
List<String> largeList = CollUtil.newArrayList();
for (int i = 0; i < 1000; i++) {
largeList.add("item" + i);
}
List<String> pageList = CollUtil.page(largeList, 1, 20); // 获取第2页,每页20条
# 四、数据入库
数据入库是将清洗后的数据保存到数据库的过程。
# 1. 单条数据入库
// 创建实体对象
Entity user = Entity.create("user")
.set("name", "张三")
.set("age", 25)
.set("email", "zhangsan@example.com")
.set("create_time", new Date());
// 插入数据
Db.use().insert(user);
// 插入数据并返回自增主键
Long id = Db.use().insertForGeneratedKey(user);
# 2. 批量数据入库
// 创建数据列表
List<Entity> userList = CollUtil.newArrayList();
for (int i = 0; i < 100; i++) {
Entity user = Entity.create("user")
.set("name", "用户" + i)
.set("age", 20 + i % 30)
.set("email", "user" + i + "@example.com")
.set("create_time", new Date());
userList.add(user);
}
// 批量插入数据
Db.use().insert(userList);
# 3. 使用事务入库
// 使用事务确保数据一致性
Db.use().tx(db -> {
// 插入用户
Entity user = Entity.create("user")
.set("name", "张三")
.set("age", 25);
Long userId = db.insertForGeneratedKey(user);
// 插入用户详情
Entity userDetail = Entity.create("user_detail")
.set("user_id", userId)
.set("address", "北京")
.set("phone", "13812345678");
db.insert(userDetail);
});
# 4. 数据更新
// 更新数据
Db.use().update(
Entity.create().set("age", 26), // 更新内容
Entity.create("user").set("name", "张三") // 更新条件
);
# 💡 完整示例
# 示例1:网页数据清洗与入库
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.http.HttpUtil;
public class WebDataCleaning {
public static void main(String[] args) {
try {
// 1. 数据抽取:获取网页内容
String url = "https://www.example.com/news";
String html = HttpUtil.get(url);
// 2. 数据抽取:提取新闻列表
List<String> newsItems = ReUtil.findAll("<div class=\"news-item\">(.*?)</div>", html, 0);
Console.log("找到 {} 条新闻", newsItems.size());
// 3. 数据转换与清洗
List<Entity> newsList = CollUtil.newArrayList();
for (String item : newsItems) {
try {
// 提取新闻标题
String title = ReUtil.get("<h3><a href=\"(.*?)\">(.*?)</a></h3>", item, 2);
String newsUrl = ReUtil.get("<h3><a href=\"(.*?)\">(.*?)</a></h3>", item, 1);
String publishDateStr = ReUtil.get("<span class=\"date\">(.*?)</span>", item, 1);
String author = ReUtil.get("<span class=\"author\">(.*?)</span>", item, 1);
String content = ReUtil.get("<p class=\"content\">(.*?)</p>", item, 1);
// 数据清洗
if (StrUtil.isBlank(title)) continue;
// 数据转换
Date publishDate = DateUtil.parse(publishDateStr);
author = StrUtil.emptyToDefault(author, "未知作者");
content = StrUtil.emptyToDefault(content, "无内容");
// 创建实体对象
Entity news = Entity.create("news")
.set("title", title)
.set("url", newsUrl)
.set("publish_date", publishDate)
.set("author", author)
.set("content", content)
.set("create_time", DateUtil.date());
newsList.add(news);
} catch (Exception e) {
Console.error("解析新闻失败: {}", e.getMessage());
}
}
// 4. 数据去重
Set<String> existingUrls = CollUtil.newHashSet();
List<Entity> uniqueNewsList = CollUtil.newArrayList();
for (Entity news : newsList) {
String url = news.getStr("url");
if (!existingUrls.contains(url)) {
existingUrls.add(url);
uniqueNewsList.add(news);
}
}
Console.log("去重后剩余 {} 条新闻", uniqueNewsList.size());
// 5. 数据入库
if (CollUtil.isNotEmpty(uniqueNewsList)) {
Db.use().insert(uniqueNewsList);
Console.log("成功入库 {} 条新闻", uniqueNewsList.size());
}
} catch (Exception e) {
Console.error("数据清洗失败: {}", e.getMessage());
}
}
}
# 示例2:CSV数据清洗与入库
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.csv.CsvReader;
import cn.hutool.csv.CsvUtil;
import cn.hutool.io.FileUtil;
public class CsvDataCleaning {
public static void main(String[] args) {
try {
// 1. 数据抽取:读取CSV文件
CsvReader reader = CsvUtil.getReader();
List<cn.hutool.csv.CsvRow> rows = reader.read(FileUtil.file("data.csv"));
Console.log("读取到 {} 行数据", rows.size());
// 2. 数据转换与清洗
List<Entity> dataList = CollUtil.newArrayList();
for (int i = 1; i < rows.size(); i++) { // 跳过表头
cn.hutool.csv.CsvRow row = rows.get(i);
try {
// 提取字段
String idStr = row.get(0);
String name = row.get(1);
String ageStr = row.get(2);
String birthdayStr = row.get(3);
String salaryStr = row.get(4);
// 数据清洗
if (StrUtil.isBlank(idStr) || StrUtil.isBlank(name)) continue;
// 数据转换
int id = NumberUtil.parseInt(idStr);
int age = StrUtil.isNotBlank(ageStr) ? NumberUtil.parseInt(ageStr) : 0;
Date birthday = StrUtil.isNotBlank(birthdayStr) ? DateUtil.parse(birthdayStr) : null;
double salary = StrUtil.isNotBlank(salaryStr) ? NumberUtil.parseDouble(salaryStr) : 0.0;
// 创建实体对象
Entity data = Entity.create("employee")
.set("id", id)
.set("name", name)
.set("age", age)
.set("birthday", birthday)
.set("salary", salary)
.set("create_time", DateUtil.date());
dataList.add(data);
} catch (Exception e) {
Console.error("解析第 {} 行数据失败: {}", i + 1, e.getMessage());
}
}
// 3. 数据去重
Set<Integer> existingIds = CollUtil.newHashSet();
List<Entity> uniqueDataList = CollUtil.newArrayList();
for (Entity data : dataList) {
Integer id = data.getInt("id");
if (!existingIds.contains(id)) {
existingIds.add(id);
uniqueDataList.add(data);
}
}
Console.log("去重后剩余 {} 条数据", uniqueDataList.size());
// 4. 数据入库
if (CollUtil.isNotEmpty(uniqueDataList)) {
Db.use().insert(uniqueDataList);
Console.log("成功入库 {} 条数据", uniqueDataList.size());
}
} catch (Exception e) {
Console.error("CSV数据清洗失败: {}", e.getMessage());
}
}
}
# 示例3:JSON数据清洗与入库
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
public class JsonDataCleaning {
public static void main(String[] args) {
try {
// 1. 数据抽取:从API获取JSON数据
String apiUrl = "https://api.example.com/users";
String jsonStr = HttpUtil.get(apiUrl);
// 2. 解析JSON数据
JSONObject json = JSONUtil.parseObj(jsonStr);
JSONArray usersArray = json.getJSONArray("users");
Console.log("获取到 {} 个用户数据", usersArray.size());
// 3. 数据转换与清洗
List<Entity> userList = CollUtil.newArrayList();
for (int i = 0; i < usersArray.size(); i++) {
try {
JSONObject userJson = usersArray.getJSONObject(i);
// 提取字段
String username = userJson.getStr("username");
String email = userJson.getStr("email");
String phone = userJson.getStr("phone");
String createdAt = userJson.getStr("created_at");
JSONObject addressJson = userJson.getJSONObject("address");
// 数据清洗
if (StrUtil.isBlank(username) || StrUtil.isBlank(email)) continue;
if (!Validator.isEmail(email)) continue;
// 数据转换
Date createTime = DateUtil.parse(createdAt);
String city = addressJson != null ? addressJson.getStr("city", "") : "";
String country = addressJson != null ? addressJson.getStr("country", "") : "";
// 标准化手机号
String standardizedPhone = StrUtil.isNotBlank(phone)
? phone.replaceAll("[^0-9+"]", "")
: "";
// 创建实体对象
Entity user = Entity.create("user")
.set("username", username)
.set("email", email)
.set("phone", standardizedPhone)
.set("city", city)
.set("country", country)
.set("created_at", createTime)
.set("update_time", DateUtil.date());
userList.add(user);
} catch (Exception e) {
Console.error("解析第 {} 个用户数据失败: {}", i + 1, e.getMessage());
}
}
// 4. 数据去重
Set<String> existingEmails = CollUtil.newHashSet();
List<Entity> uniqueUserList = CollUtil.newArrayList();
for (Entity user : userList) {
String email = user.getStr("email");
if (!existingEmails.contains(email)) {
existingEmails.add(email);
uniqueUserList.add(user);
}
}
Console.log("去重后剩余 {} 个用户数据", uniqueUserList.size());
// 5. 数据入库
if (CollUtil.isNotEmpty(uniqueUserList)) {
Db.use().insert(uniqueUserList);
Console.log("成功入库 {} 个用户数据", uniqueUserList.size());
}
} catch (Exception e) {
Console.error("JSON数据清洗失败: {}", e.getMessage());
}
}
}
# 🎨 高级技巧
# 1. 使用多线程提高效率
// 使用线程池提高数据处理效率
ExecutorService executor = Executors.newFixedThreadPool(10);
List<Future<?>> futures = CollUtil.newArrayList();
for (String item : items) {
futures.add(executor.submit(() -> {
processItem(item);
}));
}
// 等待所有任务完成
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
# 2. 分批处理大数据量
// 分批处理大数据量
List<Entity> allData = getAllData(); // 获取所有数据
int batchSize = 1000;
int total = allData.size();
int batches = NumberUtil.ceil(total, batchSize);
for (int i = 0; i < batches; i++) {
int start = i * batchSize;
int end = Math.min((i + 1) * batchSize, total);
List<Entity> batchData = allData.subList(start, end);
Db.use().insert(batchData);
Console.log("已处理 {} / {} 条数据", end, total);
}
# 3. 使用缓存减少重复请求
// 使用缓存减少重复的API请求
Cache<String, String> cache = CacheUtil.newLRUCache(1000);
String getApiData(String url) {
return cache.get(url, () -> {
return HttpUtil.get(url);
}, Duration.ofHours(1)); // 缓存1小时
}
# 4. 数据质量监控
// 数据质量监控
int total = 0;
int valid = 0;
int invalid = 0;
Map<String, Integer> errorStats = CollUtil.newHashMap();
for (String item : items) {
total++;
try {
processItem(item);
valid++;
} catch (Exception e) {
invalid++;
String errorType = e.getClass().getSimpleName();
errorStats.put(errorType, errorStats.getOrDefault(errorType, 0) + 1);
}
}
Console.log("数据质量报告:");
Console.log("总数据量: {}", total);
Console.log("有效数据: {}", valid);
Console.log("无效数据: {}", invalid);
Console.log("错误统计: {}", errorStats);
# 5. 使用事务保证数据一致性
// 使用事务保证数据一致性
Db.use().tx(db -> {
try {
// 清理旧数据
db.execute("DELETE FROM user WHERE last_active < ?", DateUtil.offsetDay(new Date(), -30));
// 插入新数据
db.insert(userList);
// 更新统计信息
db.execute("UPDATE stats SET user_count = (SELECT COUNT(*) FROM user)");
} catch (Exception e) {
// 事务会自动回滚
throw new RuntimeException("数据处理失败", e);
}
});
# ⚠️ 注意事项
# 1. 数据备份
在进行数据清洗和入库操作前,务必对原始数据和目标数据库进行备份,以防止数据丢失。
# 2. 错误处理
数据清洗过程中可能会遇到各种错误,如格式错误、网络错误、数据库错误等,务必做好异常处理。
# 3. 性能优化
- 对于大数据量,使用分批处理和多线程提高效率
- 使用连接池减少数据库连接开销
- 合理设置缓存,减少重复请求
- 优化SQL语句,避免全表扫描
# 4. 数据隐私保护
在处理敏感数据时,要注意数据隐私保护:
- 对敏感字段进行加密处理
- 避免在日志中记录敏感信息
- 遵守相关数据保护法规
# 5. 测试验证
在正式运行前,务必进行充分的测试:
- 测试数据抽取的准确性
- 测试数据转换的正确性
- 测试数据清洗的有效性
- 测试数据入库的完整性
# 📊 最佳实践
# 1. 建立数据清洗规则
在开始数据清洗前,建立明确的数据清洗规则:
- 定义数据格式标准
- 确定缺失值处理策略
- 制定数据去重规则
- 建立数据验证标准
# 2. 采用ETL流程
采用经典的ETL(Extract-Transform-Load)流程:
- Extract:从数据源抽取原始数据
- Transform:转换和清洗数据
- Load:将清洗后的数据加载到目标系统
# 3. 日志记录
详细记录数据清洗过程中的日志:
- 记录数据抽取的来源和数量
- 记录数据转换的规则和结果
- 记录数据清洗的过程和统计信息
- 记录数据入库的结果和时间
# 4. 监控和反馈
建立数据清洗的监控和反馈机制:
- 监控数据质量指标
- 定期生成数据质量报告
- 及时处理数据质量问题
- 持续优化数据清洗规则
# 5. 文档化
对数据清洗过程进行文档化:
- 记录数据源的信息
- 记录数据清洗的规则和流程
- 记录数据转换的逻辑
- 记录数据入库的表结构
# 🎯 总结
通过 Hutool 的各种工具类组合使用,可以高效地完成数据清洗的各个环节:
- 数据抽取:使用
HttpUtil、ReUtil、CsvUtil等工具从各种数据源获取数据 - 数据转换:使用
DateUtil、NumberUtil、StrUtil等工具转换数据格式 - 数据清洗:使用
CollUtil、Validator、StrUtil等工具处理数据质量问题 - 数据入库:使用
Db模块将清洗后的数据保存到数据库
Hutool 提供的工具类简洁易用,能够大大提高数据清洗的效率和质量。无论是小规模的数据清洗还是大规模的数据处理,Hutool 都能提供合适的解决方案。
在实际应用中,应根据具体需求选择合适的工具类和方法,并结合最佳实践进行数据清洗,以确保数据的准确性、完整性和一致性。
上次更新: 2026/01/03, 21:59:18