前言
最近在使用影刀RPA做一些爬虫操作,图形化的界面确实对之前没有啥爬虫知识的我非常友好,大部分功能都有完整组件,部分没封装的组件通过AI+python生成代码也能解决问题。
爬取到的数据当然可以保存到本地excel,但是为了后续使用方便,我觉得把数据存到数据库中,此时mongoDB就是一个不错的选择
关于mongoDB的特性与传统关系型数据库的对比,此处不再赘述。因为我不想在爬虫中暴露数据库连接信息,也方便后续集成其他功能,我决定用Java + MongoDB做一个后端
SpringBoot配置
JPA很方便的集成了MongoDB,但没想到第一次配置的时候就提示连接错误。明明是在我本地启动的,账号密码都是对的,为啥就是提示连接错误呢?
错误示例
spring:
data:
mongodb:
host: 127.0.0.1
port: 27017
database: reader
username: abc
password: abc123
在 MongoDB 中,authentication-database 参数指定了 验证账号和密码 的数据库。理解这一点需要明白以下两点:
- MongoDB 用户和认证数据库的关系
在 MongoDB 中,用户是与某个数据库关联的,并且账号验证的元数据(如用户名、密码)会存储在该数据库中,通常是admin数据库。
如果你的账号是在admin数据库中创建的,那么即使你连接的是其他数据库,也需要向admin数据库发送认证请求。 - 为什么认证失败?
如果你不指定authentication-database,Spring Boot 会默认将当前连接的database(在你这里是reader)作为认证数据库。这会导致以下问题:
-
- Spring Boot 发送用户名和密码时,会尝试在
reader数据库中验证。 - 但你的账号
abc和密码abc123是存储在admin数据库中的,因此认证会失败。
- Spring Boot 发送用户名和密码时,会尝试在
解决方法
在你的配置中,加上:authentication-database:admin即可解决
完整配置
spring:
data:
mongodb:
host: 127.0.0.1
port: 27017
database: reader
username: abc
password: abc123
authentication-database: admin
auto-index-creation: true
创建实体类
因为mongo的集合Bson类似json,所以可以很好的支持各种嵌套结构,也可以通过注解方便的创建索引
要让注解自动创建 需要开启配置 auto-index-creation: true 可参见上面的配置
假设你没有显式指定 _id,MongoDB 会自动为每个插入的文档生成一个 _id。
默认情况下,MongoDB 使用 ObjectId 类型来生成这个 _id,它是一个 12 字节的 BSON 类型,包含了以下信息:
- 4 字节:时间戳(从 Unix 时间戳开始,表示文档创建的时间)
- 5 字节:随机值(通常是机器 ID)
- 3 字节:进程 ID(产生文档的进程 ID)
- 3 字节:自增计数器(用于避免冲突)
这个 12 字节的 ObjectId 保证了每个生成的 ID 都是唯一的,并且按时间顺序递增。
在Java中虽然可以用String类型接收,但是在mongo中实际存储的类型是ObjectId,所以在使用比如 Navicat等工具筛选时,需要使用正确的类型
@Data
@NoArgsConstructor
@Document(collection = "users") // MongoDB 集合名为 "users"
@CompoundIndex(def = "{'username': 1, 'email': 1}") // 复合索引
public class User {
@Id
private String id; // MongoDB 的 _id 字段
@Indexed(unique = true) // 为 "username" 字段创建唯一索引
private String username;
@Indexed(unique = true) // 为 "email" 字段创建唯一索引
private String email;
private String name;
private List<String> roles;
private long createdAt;
private boolean active;
}
操作数据库
在 Spring Data MongoDB 中,主要有两种常见的方式来与 MongoDB 交互:使用
MongoRepository集成了常见的 crud方法,基本的操作已经足够使用MongoTemplate可以使用更复杂的查询,充分发挥mongo的灵活特性
下面有几个基础示例
MongoRepository
JPA的经典语法糖,仅需通过方法名定义就能生成查询简单的语句
@Query注解也可以做许多稍复杂一些的查询
伟大无需多言
@Repository
public interface UserRepository extends MongoRepository<User, String> {
// 根据名字查找用户,自动实现查询
List<User> findByName(String name);
// 根据年龄查找用户
List<User> findByAgeGreaterThan(int age);
// 查找名字为 "John" 的用户,并按年龄排序
@Query("{ 'name': 'John' }")
List<User> findByNameSortedByAge();
// 查找年龄大于等于给定值且小于 30 的用户
@Query("{ 'age': { $gte: ?0, $lt: 30 } }")
List<User> findUsersByAgeRange(int age);
}
MongoTemplate
通过构造 Query 和 Update 对象可以实现更复杂一些的场景,比如动态条件查询、直接通过条件更新等。还支持聚合查询等更为复杂的查询,这里简单介绍暂不深入
@Service
public class UserService {
@Autowired
private MongoTemplate mongoTemplate;
// 查找名字为 "John" 的用户
public List<User> getUsersByName(String name) {
Query query = new Query(Criteria.where("name").is(name));
return mongoTemplate.find(query, User.class); // 查找符合条件的用户
}
// 查找年龄大于 18 岁的用户
public List<User> getUsersByAgeGreaterThan(int age) {
Query query = new Query(Criteria.where("age").gte(age));
return mongoTemplate.find(query, User.class); // 使用 Criteria 构建查询
}
// 更新用户年龄
public void updateUserAge(String id, int newAge) {
Query query = new Query(Criteria.where("id").is(id));
Update update = new Update().set("age", newAge); // 创建更新对象
mongoTemplate.updateFirst(query, update, User.class); // 执行更新
}
}
MongoDB总结
通过上面的简单配置,就可以愉快的使用mongo了。如果你也尝试了使用Java创建集合,你会发现在你的集合中多出了一个 _class 字段,这里记录着实体类的完整类名,也可以通过配置不生成,但是在如果实体类有集成之类的其他额外属性,就很可能产生一些不必要的问题,通常还是保留这个字段
图片转码
爬虫爬取到图片的url很可能带有防盗链或者限时访问,为了防止网络图片失效,还是需要将网络图片保存到本地。网络图片的格式多种多样,一方面为了统一格式,也为了在保持画质的前提下压缩体积,我决定在图片入库之前先对图片进行转码。
WebP格式的优势
WebP 是 Google 推出的现代图像格式,旨在通过更高效的压缩算法减少文件大小,同时保持视觉质量。与传统的图像格式如 PNG、JPG、JPEG 相比, WebP 可以在无损和有损压缩下,保持较小的文件大小。并且也支持动画与透明
| 特性 | WebP | JPG/JPEG | PNG |
|---|---|---|---|
| 压缩类型 | 有损/无损 | 有损压缩 | 无损压缩 |
| 文件大小 | 更小,效率高 | 适中,文件较大 | 文件大,但质量无损 |
| 透明通道支持 | 支持 | 不支持 | 支持 |
| 动画支持 | 支持 | 不支持 | 不支持 |
| 色彩质量 | 高,支持 24 位色彩 | 高,但文件大 | 高,且无损 |
| 浏览器支持 | 新版主流浏览器全面支持 | 所有浏览器都支持 | 所有浏览器都支持 |
据我的不专业实验,同一张图片 webp格式对比 jpg 通常只有 75%~80% 的大小
起码对我来说这个画质没啥区别
Java的一大优势,直接使用现成的工具库就好,这里我使用的是 imageio
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
这里我入库的时候转为了Base64,虽然刚刚压缩的体积又被base64给浪费了,但是能不用传输文件流还是挺方便的
public class ImageUtil {
@SneakyThrows
public static String toBase64Webp(MultipartFile file) {
String fileName = Optional.ofNullable(file).filter(x -> !x.isEmpty()).map(MultipartFile::getOriginalFilename)
.filter(StrUtil::isNotBlank).orElseThrow(() -> new LevyException("文件为空"));
BufferedImage image;
try (ByteArrayInputStream bis = new ByteArrayInputStream(file.getBytes())) {
image = ImageIO.read(bis);
}
if (image == null) {
throw new LevyException("无法读取图片,请确认上传的是有效图片文件");
}
byte[] webpData;
if (fileName.endsWith(".webp")) {
webpData = file.getBytes();
} else {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "webp", baos);
webpData = baos.toByteArray();
}
}
if (webpData.length == 0) {
throw new LevyException("WebP 数据为空");
}
byte[] encode = Base64.getEncoder().encode(webpData);
String base64Content = new String(encode, StandardCharsets.UTF_8);
return "data:image/webp;base64," + base64Content;
}
}
浏览器缓存
通过日常观察我们可以发现,很多网站第一遍加载很慢,但是后面第二次第三次加载就快了。这是因为浏览器将很多静态资源缓存 (如图片、CSS、JS 等) 了 ,因为这些资源通常变化较少,缓存能显著提升加载性能。
由于我并没有使用oss存储图片,在前端使用图片时使用图片id请求后端,获取到base64后再渲染,默认就不会被浏览器缓存
给响应增加缓存
要知道如何给响应增加缓存,需要先了解为什么图片会被浏览器缓存 通过控制台查看普通图片的请求
请求方法:
- 对于 GET 请求,浏览器通常会遵循
Cache-Control头的配置进行缓存。 - 对于 POST 请求,HTTP 标准建议不要缓存响应,浏览器也会默认不缓存。
Content-Type :
- 浏览器可能会根据返回的
Content-Type判断资源是否适合缓存。 - 如果返回的是
application/json,浏览器默认会认为是动态数据,通常不会缓存。
Cache-Control:
- 例如:
Cache-Control: max-age=2592000``max-age表示缓存的有效期为 2592000 秒(即 30 天)。浏览器会在缓存有效期内直接从本地获取资源,而不会向服务器发送请求。
对于SpringBoot来说,可以很简单的给某个接口增加响应头。也可以使用其他方式通过通配符之类的配置,不过目前我没有这个需求
@GetMapping("/{id}")
public ResponseEntity<Result<Image>> getImageById(@PathVariable String id) {
Image image = repository.findById(id).orElseThrow(() -> new LevyException("未找到图片"));
Result<Image> imageResult = Result.success(image);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.body(imageResult);
}
关于为什么不适用localStorage缓存,因为对于单个域名是有限制的,而静态资源很快会将这个上线占满
| 特点 | 浏览器缓存(HTTP 缓存) | localStorage |
|---|---|---|
| 数据存储 | 存储服务器返回的 HTTP 响应数据。 | 存储键值对数据(字符串)。 |
| 存储数据类型 | 资源文件:HTML、CSS、JS、图片、API 响应等。 | 只能存储文本字符串。 |
| 数据存储方式 | 由服务器通过 HTTP 响应头 控制缓存。 | 由客户端代码显式设置和管理。 |
| 存储大小 | 依赖于浏览器缓存机制(通常较大)。 | 每个域名限制 5-10MB 大小。 |
| 管理自动化程度 | 自动管理,遵循 HTTP 规范(Cache-Control等)。 | 需要开发者手动编写逻辑管理。 |
| 生命周期 | 取决于缓存头设置,自动过期或更新。 | 数据持久保存,除非手动清理。 |
| 安全性 | 有一定的安全限制,不能跨域。 | 也不能跨域,且容易被篡改。 |
| 适用场景 | 静态资源、接口数据的缓存。 | 配置参数、用户偏好、状态数据等。 |
后话
第一次使用爬虫和mongo的总结,文章混杂到了一起,但是在这个过程中确实解决了不少问题。虽然种种原因很多业务系统都不会采用mongo,但是自己用用才能体会到mongo的便捷