【Java】爬取澳门区划信息
官网地址:
https://macaostreets.iam.gov.mo/zh_mo/freguesiaindex.html
大区部分是在页面展示的
点击发现并没有请求网络,所以数据是js中存在的
找到了展示街道方法,这一段:
使用大区id匹配上述变量的
function showStreets(freguesia){ var freguesiaStreets; switch(freguesia){ case "fatima": freguesiaStreets = fatimaStreets; break; case "lourenco": freguesiaStreets = lourencoStreets; break; case "lazaro": freguesiaStreets = lazaroStreets; break; case "carno": freguesiaStreets = carnoStreets; break; case "se": freguesiaStreets = seStreets; break; case "antonio": freguesiaStreets = antionioStreets; break; case "xavier": freguesiaStreets = xavierStreets; break; }
可以发现,这一段是js代码实现的,我可以通过Jsoup解析文档获取js的源代码部分
但是要怎么在Java读取JS变量呢?
意外发现JDK8自带提供API 【JS解析引擎】
但是有使用限制,只支持ES5的语法 ,并且不支持任何第三方库的语法内容
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager;
网上的资料不多, 要边写边看才知道
@SneakyThrows public static void main(String[] args) { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("Nashorn"); engine.eval("var obj = { a: 100, b: true, c: 'hello js-engine'}"); ScriptObjectMirror jsObject = (ScriptObjectMirror) engine.get("obj"); String[] ownKeys = jsObject.getOwnKeys(false); /* false表示只需要一般属性, true表示全部属性,包括对象原型的属性 */ for (String ownKey : ownKeys) { Object o = jsObject.get(ownKey); System.out.println(o); /* 按具体类型强转即可。集合、对象类型,还是强转为ScriptObjectMirror来读取 */ } }
db.setting数据源配置文件:
## db.setting文件 url = jdbc:mysql://localhost:3308/my-info?serverTimezone=Asia/Shanghai user = root pass = 123456 ## 可选配置 # 是否在日志中显示执行的SQL showSql = true # 是否格式化显示的SQL formatSql = false # 是否显示SQL参数 showParams = true # 打印SQL的日志等级,默认debug,可以是info、warn、error sqlLevel = debug #---------------------------------------------------------------------------------------------------------------- ## 连接池配置项 #———————————————— #版权声明:本文为CSDN博主「soulCoke」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 #原文链接:https://blog.csdn.net/qq_36328170/article/details/105687633 ## ---------------------------------------------------- Druid # 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 initialSize = 1 # 最大连接池数量 maxActive = 8 # 最小连接池数量 minIdle = 0 # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后, 缺省启用公平锁,并发效率会有所下降, 如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 maxWait = 0 # 是否缓存preparedStatement,也就是PSCache。 PSCache对支持游标的数据库性能提升巨大,比如说oracle。 在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。作者在5.5版本中使用PSCache,通过监控界面发现PSCache有缓存命中率记录, 该应该是支持PSCache。 poolPreparedStatements = false # 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true。 在Druid中,不会存在Oracle下PSCache占用内存过多的问题, 可以把这个数值配置大一些,比如说100 maxOpenPreparedStatements = -1 # 用来检测连接是否有效的sql,要求是一个查询语句。 如果validationQuery为null,testOnBorrow、testOnReturn、 testWhileIdle都不会其作用。 validationQuery = SELECT 1 # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 testOnBorrow = true # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 testOnReturn = false # 建议配置为true,不影响性能,并且保证安全性。 申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 testWhileIdle = false # 有两个含义: 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 timeBetweenEvictionRunsMillis = 60000 # 物理连接初始化的时候执行的sql connectionInitSqls = SELECT 1 # 属性类型是字符串,通过别名的方式配置扩展插件, 常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall # filters = stat # 类型是List<com.alibaba.druid.filter.Filter>, 如果同时配置了filters和proxyFilters, 是组合关系,并非替换关系 # proxyFilters =
工具类代码:
package cn.cloud9.chinese.mazu; import cn.hutool.core.date.DateUtil; import cn.hutool.db.Db; import cn.hutool.db.Entity; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import jdk.nashorn.api.scripting.ScriptObjectMirror; import lombok.Builder; import lombok.Data; import lombok.SneakyThrows; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.time.LocalDateTime; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; public class MaZuRegionUtil { private static final Db db = Db.use(); private static String currentTableName = ""; /* 首页 */ private static final String INDEX_PAGE = "https://macaostreets.iam.gov.mo/zh_mo/freguesiaindex.html"; /* 官网根地址 */ private static final String ROOT_PATH = "https://macaostreets.iam.gov.mo"; @Data @Builder public static final class MaZuRegionPO { private Integer id; private Integer parentId; private String name; private Integer level; private String link; private String fullPath; private String description; private byte[] image; private LocalDateTime genTime; } /* js 事件匹配数据对象 */ private static Map<String, String> REGION_MAP = new LinkedHashMap<String, String>(){{ this.put("fatima", "fatimaStreets"); this.put("lourenco", "lourencoStreets"); this.put("lazaro", "lazaroStreets"); this.put("carno", "carnoStreets"); this.put("se", "seStreets"); this.put("antonio", "antionioStreets"); this.put("xavier", "xavierStreets"); }}; @SneakyThrows private static void writeToDB(MaZuRegionPO po) { db.insert( Entity.create(currentTableName) .set("id", po.getId()) .set("parent_id", po.getParentId()) .set("full_path", po.getFullPath()) .set("name", po.getName()) .set("link", po.getLink()) .set("level", po.getLevel()) .set("description", po.getDescription()) .set("image", po.getImage()) .set("gen_time", LocalDateTime.now()) ); } @SneakyThrows public static void initialTableSpace() { String format = "`macao-region-" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + "`"; final String SQL = "CREATE TABLE IF NOT EXISTS " + format + " (" + " `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'," + " `parent_id` int NOT NULL COMMENT '上级id'," + " `name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称'," + " `link` varchar(192) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '链接'," + " `full_path` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '完整路径'," + " `level` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '层级'," + " `description` varchar(2048) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述'," + " `image` MEDIUMBLOB DEFAULT NULL COMMENT '图片'," + " `gen_time` datetime DEFAULT NULL COMMENT '记录创建时间'," + " PRIMARY KEY (`id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='澳门区域表';"; db.execute(SQL, null); currentTableName = format; } /** * 解析script标签中的js代码 * @param targetScript * @return */ @SneakyThrows private static ScriptEngine readJavaScriptData(String targetScript) { String targetJavaScriptCode = targetScript; // System.out.println(targetJavaScriptCode); /* 只保留前段变量代码 */ int index = targetJavaScriptCode.indexOf("function"); targetJavaScriptCode = targetJavaScriptCode.substring(0, index); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("Nashorn"); engine.eval(targetJavaScriptCode); return engine; } public static void goRead() { initialTableSpace(); String string = HttpUtil.get(INDEX_PAGE); // System.out.println(string); Document document = Jsoup.parse(string); /* 先解析目标JS代码变量 */ Elements scriptList = document.getElementsByTag("script"); Element targetScript = scriptList.get(9); ScriptEngine scriptEngine = readJavaScriptData(targetScript.html()); /* 提取页面区域部分 */ Elements childList = document.select("#FreguesiaSectionText > [id]"); int idx = 100; for (Element eachSector : childList) { String idKey = eachSector.id(); MaZuRegionPO sector = MaZuRegionPO.builder() .id(idx) .name(eachSector.ownText()) .fullPath(eachSector.ownText()) .level(1) .link(INDEX_PAGE) .parentId(0) .build(); // System.out.println(sector); writeToDB(sector); String jsDataKey = REGION_MAP.get(idKey); ScriptObjectMirror streetsList = (ScriptObjectMirror) scriptEngine.get(jsDataKey); String[] ownKeys = streetsList.getOwnKeys(false); int c1 = 1; for (String key : ownKeys) { ScriptObjectMirror jsObject = (ScriptObjectMirror)streetsList.get(key); String name = jsObject.get("name").toString(); String link = jsObject.get("link").toString(); // https://macaostreets.iam.gov.mo/ + link link = ROOT_PATH + link; String detailPage = HttpUtil.get(link); Document detailDoc = Jsoup.parse(detailPage); /* 读取简介 */ Element descElement = detailDoc.select(".SpotInfo > .SpotInfoText").get(0); String text = descElement.html(); /* 读取目标图片 */ Element linkA = detailDoc.select("a[data-caption='" + name + "']").get(0); String href = linkA.attr("href"); href = ROOT_PATH + href; HttpResponse execute = HttpUtil.createGet(href).execute(); byte[] bodyBytes = execute.bodyBytes(); MaZuRegionPO street = MaZuRegionPO.builder() .id(idx + c1) .parentId(sector.getId()) .name(name) .fullPath(sector.getFullPath() + " -> " + name) .level(2) .description(text) .image(bodyBytes) .link(link) .build(); writeToDB(street); c1 += 1; } idx += 100; } } @SneakyThrows public static void main(String[] args) { goRead(); } }