使用SpringBoot将中国地震台网数据保存PostGIS数据库实践

目录

前言

一、数据转换

1、Json转JavaBean

2、JavaBean与数据库字段映射

二、空间数据表设计

1、表结构设计

三、PostGIS数据保存

1、Mapper接口定义

2、Service逻辑层实现

3、数据入库

4、运行实例及结果

总结


前言

在上一篇博客中基于Java的XxlCrawler网络信息爬取实战-以中国地震台网为例,我们使用Java语言采用XxlCrawer组件进行中国地震台网数据的爬取,相信大家对如何抓取这种接口数据一定有了基本的认识,也掌握了如何基于XxlCrawer来实现自己的信息爬取实战。但是在前面的博客当中,我们仅仅是将信息爬取下来。为了在我们自己的应用系统中来应用这些基础数据,需要我们将爬取的数据进行存储起来。由于地震数据包含了空间位置信息,为方便进行空间分析的实现。这里我们将爬取的中国地震信息存储到PostGIS空间数据库中,为下一步的数据分析和可视化呈现奠定良好的基础。

本文即紧紧围绕着将信息保存到空间数据库的目标,重点讲解如何实现将中国地震台网爬取的地震信息保存到PostGIS空间数据库中。首先讲解在爬取过程中数据格式和响应数据类型的转换,将网站回传的json数据转成符合Java命名规范的数据。然后介绍台网地震信息表的设计,如何构建空间数据表。再次介绍如何将爬取的数据调用Mybatis-Plus组件实现批量入库。如果您当前也有对地震等地质灾害数据进行分析的需求,不妨看看本博文。

一、数据转换

在前面的博客中有提到,在中国地震台网中展示接口数据,将请求数据在浏览器中进行查看。可以看到以下的格式:

通过这个接口可以看到,官方返回的数据中,其所有的字段名都是大写的,如下所示:

bash 复制代码
AUTO_FLAG: "M"
CATA_ID: "CD20240413222636.00"
CATA_TYPE: ""
EPI_DEPTH: 9
EPI_LAT: "24.05"
EPI_LON: "121.60"
EQ_CATA_TYPE: ""
EQ_TYPE: "M"
IS_DEL: ""
LOCATION_C: "台湾花莲县"
LOCATION_S: ""
LOC_STN: "0"
M: "4.2"
M_MB: "0"
M_MB2: "0"
M_ML: "0"
M_MS: "0"
M_MS7: "0"
NEW_DID: "CD20240413222636"
O_TIME: "2024-04-13 22:26:35"
O_TIME_FRA: "0"
SAVE_TIME: "2024-04-13 22:35:56"
SUM_STN: "0"
SYNC_TIME: "2024-04-13 22:35:56"
id: "46396"

为了让更好的使用Java语言进行开发,使这些变量名变得更加合适,同时满足Java的编程规范。因此办结来重点讲解怎么将 json返回的数据转成java的合理变量名。

1、Json转JavaBean

如上所言,在使用XxlCrawler进行信息爬取之后,返回的数据名称不太符合Java的命名规则。那针对这种需求,有没有什么办法来进行调整呢。答案是肯定的,不管是用Gson或者fastJson,这些设计良好的json处理框架其实都包含了Json对象与JavaBean对象的互相转换。当给定格式不符合Java命名规范的属性名,可以通过注解映射的方式修改成符合Java编码规范的变量。由于本实例中采用的是Gson组件,因此给出的示例代码也是基于Gson来实现的,其它的实现组件请自行搜索相关知识,根据官方文档的知识来进行设置。

在Gson中,主要是采用@SerializedName("AUTO_FLAG")这个注解,注解后面的字段是通过接口返回的数据字段。把这个注解配置到属性中,表示当前属性对应哪个接口的字段。关键代码如下:

java 复制代码
package com.yelang.project.extend.earthquake.domain.crawler;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class CeicEarthquake implements Serializable{
	private static final long serialVersionUID = -1212153879708670015L;
	private Long pkId;//主键
	@SerializedName("AUTO_FLAG")
	private String autoFlag;
	@SerializedName("CATA_ID")
	private String cataId;
	@SerializedName("CATA_TYPE")
	private String cataType;
	@SerializedName("EPI_DEPTH")
	private BigDecimal epiDepth = new BigDecimal("0.0");
	@SerializedName("EPI_LAT")
	private String epiLat;//纬度
	@SerializedName("EPI_LON")
	private String epiLon;
	@SerializedName("EQ_CATA_TYPE")
	private String eqCataType;
	@SerializedName("EQ_TYPE")
	private String eqType;
	@SerializedName("IS_DEL")
	private String isDel;
	@SerializedName("LOCATION_C")
	private String locationC;
	@SerializedName("LOCATION_S")
	private String locationS;
	@SerializedName("LOC_STN")
	private String locStn;
	@SerializedName("M")
	private String m;
	@SerializedName("M_MB")
	private String mmb;
	@SerializedName("M_MB2")
	private String mmb2;
	@SerializedName("M_ML")
	private String mml;
	@SerializedName("M_MS")
	private String mms;
	@SerializedName("M_MS7")
	private String mms7;
	@SerializedName("NEW_DID")
	private String newDid;
	@SerializedName("O_TIME")
	private Date oTime;
	@SerializedName("O_TIME_FRA")
	private String oTimeFra;
	@SerializedName("SAVE_TIME")
	private Date saveTime;
	@SerializedName("SUM_STN")
	private String sumStn;
	@SerializedName("SYNC_TIME")
	private Date syncTime;
	@SerializedName("id")
	private String epiId;
}

通过以上的代码就可以实现将接口返回的参数映射成符合我们需求的JavaBean。

2、JavaBean与数据库字段映射

众所周知,数据的命名一般是用小写,而且单词之间一般使用下划线连接起来。而Java中对属性的命名与数据库的字段还是有一定的差异。好在我们采用的是Mybatis_Plus这个框架,可以实现数据库字段和JavaBean的对应。为了后续介绍方便,这里将直接给出空间字段的设置。以实体类代码的形式给出。在Mybatis_Plus中,主要采用@TableField(value="cata_type")来进行数据库字段的设置。完整的代码如下所示:

java 复制代码
package com.yelang.project.extend.earthquake.domain.crawler;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.google.gson.annotations.SerializedName;
import com.yelang.framework.handler.PgGeometryTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@TableName(value ="biz_ceic_earthquake",autoResultMap = true)
public class CeicEarthquake implements Serializable{
	private static final long serialVersionUID = -1212153879708670015L;
	@TableId(value="pk_id")
	private Long pkId;//主键
	@SerializedName("AUTO_FLAG")
	@TableField(value="auto_flag")
	private String autoFlag;
	@SerializedName("CATA_ID")
	@TableField(value="cata_id")
	private String cataId;
	@SerializedName("CATA_TYPE")
	@TableField(value="cata_type")
	private String cataType;
	@SerializedName("EPI_DEPTH")
	@TableField(value="epi_depth")
	private BigDecimal epiDepth = new BigDecimal("0.0");
	@SerializedName("EPI_LAT")
	@TableField(value="epi_lat")
	private String epiLat;//纬度
	@SerializedName("EPI_LON")
	@TableField(value="epi_lon")
	private String epiLon;
	@SerializedName("EQ_CATA_TYPE")
	@TableField(value="eq_cata_type")
	private String eqCataType;
	@SerializedName("EQ_TYPE")
	@TableField(value="eq_type")
	private String eqType;
	@SerializedName("IS_DEL")
	@TableField(value="is_del")
	private String isDel;
	@SerializedName("LOCATION_C")
	@TableField(value="location_c")
	private String locationC;
	@SerializedName("LOCATION_S")
	@TableField(value="location_s")
	private String locationS;
	@SerializedName("LOC_STN")
	@TableField(value="loc_stn")
	private String locStn;
	@SerializedName("M")
	@TableField(value="m")
	private String m;
	@SerializedName("M_MB")
	@TableField(value="mmb")
	private String mmb;
	@SerializedName("M_MB2")
	@TableField(value="mmb2")
	private String mmb2;
	@SerializedName("M_ML")
	@TableField(value="mml")
	private String mml;
	@SerializedName("M_MS")
	@TableField(value="mms")
	private String mms;
	@SerializedName("M_MS7")
	@TableField(value="mms7")
	private String mms7;
	@SerializedName("NEW_DID")
	@TableField(value="new_did")
	private String newDid;
	@SerializedName("O_TIME")
	@TableField(value="o_time")
	private Date oTime;
	@SerializedName("O_TIME_FRA")
	@TableField(value="o_time_fra")
	private String oTimeFra;
	@SerializedName("SAVE_TIME")
	@TableField(value="save_time")
	private Date saveTime;
	@SerializedName("SUM_STN")
	@TableField(value="sum_stn")
	private String sumStn;
	@SerializedName("SYNC_TIME")
	@TableField(value="sync_time")
	private Date syncTime;
	@SerializedName("id")
	@TableField(value="epi_id")
	private String epiId;
	@TableField(typeHandler = PgGeometryTypeHandler.class)
	private String geom;
	@TableField(exist=false)
	private String geomJson;
}

二、空间数据表设计

在很多的技术博客当中,都提到过如何进行空间数据库的设计,与常规的关系型数据库表不一样的是,空间数据库多了空间信息的存储的查询。以地震信息为例,就包含了其经纬度坐标信息。因此这里使用PostGIS作为空间数据库存储空间信息。

在讲解改表是,我们首先根据接口的字段来定义其关键属性字段,然后自己设计Geometry字段,通过爬取的经纬度信息来生成Geometry信息,然后保存到相应的字段当中。

1、表结构设计

主要表结构设计如下,包含了通过接口返回的基本信息:

生成出来的SQL语句如下,如果需要的话,可以直接使用,这里直接提供,供大家参考。

sql 复制代码
CREATE TABLE "public"."biz_ceic_earthquake" (
  "pk_id" int8 NOT NULL,
  "auto_flag" varchar(30) COLLATE "pg_catalog"."default",
  "cata_id" varchar(30) COLLATE "pg_catalog"."default",
  "cata_type" varchar(30) COLLATE "pg_catalog"."default",
  "epi_depth" numeric(11,8),
  "epi_lat" varchar(15) COLLATE "pg_catalog"."default",
  "epi_lon" varchar(15) COLLATE "pg_catalog"."default",
  "eq_cata_type" varchar(30) COLLATE "pg_catalog"."default",
  "eq_type" varchar(30) COLLATE "pg_catalog"."default",
  "is_del" varchar(6) COLLATE "pg_catalog"."default",
  "location_c" varchar(255) COLLATE "pg_catalog"."default",
  "location_s" varchar(100) COLLATE "pg_catalog"."default",
  "loc_stn" varchar(20) COLLATE "pg_catalog"."default",
  "m" varchar(10) COLLATE "pg_catalog"."default",
  "mmb" varchar(10) COLLATE "pg_catalog"."default",
  "mmb2" varchar(10) COLLATE "pg_catalog"."default",
  "mml" varchar(10) COLLATE "pg_catalog"."default",
  "mms" varchar(10) COLLATE "pg_catalog"."default",
  "mms7" varchar(10) COLLATE "pg_catalog"."default",
  "new_did" varchar(16) COLLATE "pg_catalog"."default",
  "o_time" timestamp(6),
  "o_time_fra" varchar(10) COLLATE "pg_catalog"."default",
  "save_time" timestamp(6),
  "sum_stn" varchar(10) COLLATE "pg_catalog"."default",
  "sync_time" timestamp(6),
  "epi_id" varchar(10) COLLATE "pg_catalog"."default",
  "geom" "public"."geometry",
  CONSTRAINT "pk_biz_ceic_earthquake" PRIMARY KEY ("pk_id")
);
CREATE INDEX "idx_biz_ceic_earthquake_eqidept" ON "public"."biz_ceic_earthquake" USING btree (
  "epi_depth" "pg_catalog"."numeric_ops" ASC NULLS LAST
);
CREATE INDEX "idx_biz_ceic_earthquake_geom" ON "public"."biz_ceic_earthquake" USING gist (
  "geom" "public"."gist_geometry_ops_2d"
);
CREATE INDEX "idx_biz_ceic_earthquake_m" ON "public"."biz_ceic_earthquake" USING btree (
  "m" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
);
COMMENT ON COLUMN "public"."biz_ceic_earthquake"."pk_id" IS '主键id';

为了在查询的时候提高查询性能,我们建立三个索引,两个普通索引和一个空间索引,构建在geom这个字段上的。后期如果要做空间分析可以使用空间索引进行查询效率提升。

三、PostGIS数据保存

设计好了地震信息空间数据表,信息接口也进行了定义。万事俱备只欠东风,只需要采用Mybatis_Plus组件将爬取的信息通过接口保存到PostGIS空间数据库中即可。在第一节中其实已经将实现接口参数转换成数据库字段,关于实体类的定义在此不赘述,这里只将数据入库的流程和方法进行简单介绍。ORM框架采用Mybatis_Plus框架。

1、Mapper接口定义

mapper接口相当于是数据库操作的总入口,由于这里仅演示如何插入数据,暂时没有其它的业务需求,因此接口中除集成的方法,暂不新增新的方法。

java 复制代码
package com.yelang.project.extend.earthquake.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yelang.project.extend.earthquake.domain.crawler.CeicEarthquake;
public interface CeicEarthquakeMapper extends BaseMapper<CeicEarthquake>{
}

2、Service逻辑层实现

业务逻辑层也比较简单,为了演示效果,同样不增加额外的方法,仅实现MP自带的批量插入功能来实现数据插入。

java 复制代码
package com.yelang.project.extend.earthquake.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yelang.project.extend.earthquake.domain.crawler.CeicEarthquake;
import com.yelang.project.extend.earthquake.mapper.CeicEarthquakeMapper;
import com.yelang.project.extend.earthquake.service.ICeicEarthquakeService;
@Service
public class CeicEarthquakeServiceImpl extends ServiceImpl<CeicEarthquakeMapper, CeicEarthquake> implements ICeicEarthquakeService{
}

3、数据入库

数据入库主要是调用ICeicEarthquakeService的批量插入方法。这里采用Junit测试套件进行爬取测试。实际情况可以内置到SpringBoot的一个方法或者定时任务当中。在这里需要注意的一个地方就是,我们在数据库中定义了一个Geometry字段来存储空间点信息。因此在信息爬取过程中需要动态生成,主要是手动构造Wkt格式的数据,通过PgGeometryTypeHandler来实现空间类型转换,看过博客的朋友应该对这种操作方法很熟悉。爬取及入库的代码如下:

java 复制代码
package com.yelang.project;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.xuxueli.crawler.XxlCrawler;
import com.xuxueli.crawler.parser.strategy.NonPageParser;
import com.yelang.common.utils.StringUtils;
import com.yelang.project.extend.earthquake.domain.crawler.CeicDateAdapter;
import com.yelang.project.extend.earthquake.domain.crawler.CeicEarthquake;
import com.yelang.project.extend.earthquake.domain.crawler.CeicEarthquakeCrawler;
import com.yelang.project.extend.earthquake.service.ICeicEarthquakeService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestXxlCrawerCeic {
	@Autowired
	private ICeicEarthquakeService service;
	@Test
	public void testGetCeic() {
		String commonUrl = "https://www.ceic.ac.cn/ajax/search?start=&&end=&&jingdu1=&&jingdu2=&&weidu1=&&weidu2=&&height1=&&height2=&&zhenji1=&&zhenji2=&_="
				+ System.currentTimeMillis();
		String[] urlList = new String[20];
		urlList[0] = commonUrl + "&&page=" + 1;
		// 构造爬虫
		XxlCrawler crawler = new XxlCrawler.Builder().setUrls(urlList).setThreadCount(3).setPauseMillis(3000)
				.setUserAgent(
						"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36")
				.setIfPost(false).setFailRetryCount(3)// 重试三次
				.setPageParser(new NonPageParser() {
					public void parse(String url, String pageSource) {
						if (!StringUtils.isBlank(pageSource)) {
							pageSource = pageSource.substring(1, pageSource.length() - 1);
							Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new CeicDateAdapter()).create();
							CeicEarthquakeCrawler crawler = gson.fromJson(pageSource, CeicEarthquakeCrawler.class);
							System.out.println("总页数:"+crawler.getNum());
							for (CeicEarthquake data : crawler.getShuju()) {
								String geom = "SRID=" + 4326 +";POINT (" + data.getEpiLon()+ " "+data.getEpiLat()+")";//拼接srid,实现动态写入
								System.out.println(data);
								System.out.println(geom);
								data.setGeom(geom);
							}
							service.saveBatch(crawler.getShuju(), 300);
						}
					}
				}).build();
		crawler.start(true);// 启动
	}
}

上面代码的关键就是WKT的构造,默认采用4326坐标系:

java 复制代码
String geom = "SRID=" + 4326 +";POINT (" + data.getEpiLon()+ " "+data.getEpiLat()+")";//拼接srid,实现动态写入

4、运行实例及结果

使用Junit的测试套件运行上述方法,在控制台可以看到如下的打印结果:

很明显在控制台中看到批量插入语句和信息爬取信息,示例信息如下:

java 复制代码
CeicEarthquake(pkId=null, autoFlag=M, cataId=CD20240407190310.00, cataType=, epiDepth=18, epiLat=41.89, epiLon=82.17, eqCataType=, eqType=M, isDel=, locationC=新疆阿克苏地区拜城县, locationS=, locStn=0, m=4.2, mmb=0, mmb2=0, mml=0, mms=0, mms7=0, newDid=CD20240407190310, oTime=Sun Apr 07 19:03:09 CST 2024, oTimeFra=0, saveTime=Sun Apr 07 19:08:03 CST 2024, sumStn=0, syncTime=Sun Apr 07 19:08:03 CST 2024, epiId=46366, geom=null, geomJson=null)
SRID=4326;POINT (82.17 41.89)
CeicEarthquake(pkId=null, autoFlag=M, cataId=CD20240407182216.00, cataType=, epiDepth=15, epiLat=41.91, epiLon=82.00, eqCataType=, eqType=M, isDel=, locationC=新疆阿克苏地区拜城县, locationS=, locStn=0, m=3.0, mmb=0, mmb2=0, mml=0, mms=0, mms7=0, newDid=CD20240407182216, oTime=Sun Apr 07 18:22:16 CST 2024, oTimeFra=0, saveTime=Sun Apr 07 18:27:58 CST 2024, sumStn=0, syncTime=Sun Apr 07 18:27:58 CST 2024, epiId=46365, geom=null, geomJson=null)
SRID=4326;POINT (82.00 41.91)
19:43:44.403 [pool-2-thread-2] DEBUG c.y.p.e.e.m.C.insert - [debug,137] - ==>  Preparing: INSERT INTO biz_ceic_earthquake ( pk_id, auto_flag, cata_id, cata_type, epi_depth, epi_lat, epi_lon, eq_cata_type, eq_type, is_del, location_c, location_s, loc_stn, m, mmb, mmb2, mml, mms, mms7, new_did, o_time, o_time_fra, save_time, sum_stn, sync_time, epi_id, geom ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
19:43:44.548 [pool-2-thread-2] DEBUG c.y.p.e.e.m.C.insert - [debug,137] - ==> Parameters: 1780562787278299138(Long), M(String), CD20240411005512.00(String), (String), 12(BigDecimal), 24.14(String), 121.87(String), (String), M(String), (String), 台湾花莲县海域(String), (String), 0(String), 4.4(String), 0(String), 0(String), 0(String), 0(String), 0(String), CD20240411005512(String), 2024-04-11 00:55:12.0(Timestamp), 0(String), 2024-04-11 01:05:21.0(Timestamp), 0(String), 2024-04-11 01:05:21.0(Timestamp), 46385(String), SRID=4326;POINT(121.87 24.14)(PGgeometry)

最后来看一下在PostGIS当中是否将数据成功入库,在客户端中执行以下查询语句:

sql 复制代码
select * from biz_ceic_earthquake;

通过上述界面看到,通过XxlCrawler爬取的信息就成功的保存到了PostGIS空间数据库中。

总结

以上就是本文的主要内容,本文即紧紧围绕着将信息保存到空间数据库的目标,重点讲解如何实现将中国地震台网爬取的地震信息保存到PostGIS空间数据库中。首先讲解在爬取过程中数据格式和响应数据类型的转换,将网站回传的json数据转成符合Java命名规范的数据。然后介绍台网地震信息表的设计,如何构建空间数据表。再次介绍如何将爬取的数据调用Mybatis-Plus组件实现批量入库。行文仓促,定有不足,欢迎朋友们在评论浏览批评指正,不胜感激。