哇标题看起来挺厉害的,其实没有很难啦,而且非常有趣。
我为什么要做这个
我本人就很讨厌看到这种标题——我为什么xxx?喵喵喵?你为什么xxx关我什么事?唉,人总是会变成自己讨厌的人嘛。
换个说法,说说做这个的初衷,其实没有什么初衷,被逼的。
为了尽早完成毕业设计可以出去打工,我选择提前联系导师。于是就被提供了两个题目,我决定做水利相关的知识图谱。你看,多么河海特色!水水水,离开水就不能活了,没毛病。
于是先从图数据库入手,neo4j是个好东西,就决定是你了。在构建复杂的水水水相关实体前,我先拿中国行政区划数据试个水,逻辑相对简单,就一种隶属关系。
献上美图:
起步
首先,准备一下原料,中国行政区划元数据,不求完全结构化,起码半结构化,不然会很痛苦,毕竟有70W+数据。
Neo4j一套,python或者什么脚本语言一款,脑子一坨,足够了。
首先我拿到70W+的XML文件,哎哟我去,ls
一下都卡半天,我都害怕python会不会应付不来,跑一遍几小时可能就太惨了。
首先用python列出几个文件:
files = os.listdir(datas_path)
print(files[0])
哈哈哈,我先搞个文件名,然后直接打开这个文件,避免了「我不能ls就不知道文件名,不知道文件名就不能看内容」的问题。可以看到里面的大致结构,这里取一部分:
<mdExtInfo>
<obj_att>
<AD_GRAD>
<key>行政区划级别</key>
<value>村</value>
</AD_GRAD>
<AD_CODE>
<key>行政区划代码</key>
<value>441323121207</value>
</AD_CODE>
<UP_AD_NAME>
<key>上级行政区划名称</key>
<value>白盆珠镇</value>
</UP_AD_NAME>
<AD_FULL_NAME>
<key>行政区划全称</key>
<value>广东省-惠州市-惠东县-白盆珠镇-布心村民委员会</value>
</AD_FULL_NAME>
<AD_NAME>
<key>行政区划名称</key>
<value>布心村民委员会</value>
</AD_NAME>
</obj_att>
</mdExtInfo>
枚举行政区划级别
唉,几十万文件,我都不知道有几种行政区,先跑一遍看看有哪些区划级别。
核心代码:
cates = set()
for file in files:
doc = parse(datas_path + file)
for item in doc.iterfind('mdExtInfo/obj_att/AD_GRAD'):
title = item.findtext('value')
cates.add(title)
print(title, file, cates)
大概不到十分钟就能跑完,最后得到一共有6种级别:县级、村、国家级、乡级、地市级、省级。
给这些文件分类
每次都跑十分钟太过分了,而且我发现,不同的级别,XML里的结构不同,比如省级有十几个字段,乡级只有五个。所以分类是必须的。
按照不同级别,将同级别的文件名放在一个文件里,方便以后遍历。
到这里,我突然明白了一些道理,来来来,给自己加点戏!
本来以为都是非结构化数据,刚好有在看机器学习,哇可以学以致用了,把数据扔进去,自动聚类,开心的当上调参男孩…… 然而我发现数据格式化的挺不错,就开始自己去找他们的特征,这特喵不就是——人肉学习嘛!然而再反过来想,机器学习到底是什么呢?
构造 Neo4j 需要的 CSV
抽取属性
这么一大坨数据要导入的,用CQL实在太慢,生成指定格式的CSV文件就OK啦~真简单~
AD_CODE,AD_NAME,AD_GRAD,AD_AREA,AD_FULL_NAME,AD_ABBR_NAME,LOW_LEFT_LONG,UP_RIGHT_LONG,UP_RIGHT_LAT,AD_STAT_LAT,AD_STAT,LOW_LEFT_LAT,AD_STAT_LONG
610000000000,陕西省,省级,197025.84,陕西省,陕西,105.4872,111.2422,39.58533,34.26645358,西安市新城区新城大院,31.70674,108.94952476
650000000000,新疆维吾尔自治区,省级,1660000,新疆维吾尔自治区,新疆,73.49989,96.38728,49.18006,43.79179105,乌鲁木齐市中山路479号,34.33374,87.62484437
130000000000,河北省,省级,187700,河北省,河北,113.4551,119.8485,42.61558,38.03705206,石家庄市长安区裕华东路113号,36.04881,114.52429283
...
哈哈哈哈,先把它们有用的属性都拿出来,按级别分类放到6个CSV文件中,结构如上,从此就可以舒服很多了,毕竟不用遍历几十万个文件了。
构建行政单位实体
等…… 等一下……
其实在这之前,我发现了很多坑爹的地方,比如地市级单位有14个属性,但有个别数据只有几个属性,所以遍历的时候一定要判断,不存在的话要用空字符串替代,不然CSV对不齐后面就会更坑。
首先做个属性映射,我不想存到数据库里字段都是瞎眼的大写:
prop_maps = {
'AD_CODE': 'code:ID(AD)',
'AD_NAME': 'name',
# 'AD_GRAD': 'level',
'AD_AREA': 'area:double',
'AD_FULL_NAME': 'full_name',
'AD_ABBR_NAME': 'abbr_name',
'LOW_LEFT_LONG': 'low_left_longtitude:double',
'UP_RIGHT_LONG': 'up_right_longtitude:double',
'UP_RIGHT_LAT': 'up_right_latitude:double',
'AD_STAT_LAT': 'station_latitude:double',
'AD_STAT': 'station',
'LOW_LEFT_LAT': 'low_left_latitude:double',
'AD_STAT_LONG': 'station_longtitude:double'
# 'UP_AD_NAME': 'father_name'
}
根据 Neo4j Import Tool 上的格式,我们要注意ID字段,以及LABEL字段,我决定给它们两个标签,一个是级别,比如Province,一个是AD,表示Administrative Division 行政区划,毕竟这个数据库还会有很多水水水的数据进来。
code:ID(AD),name,area:double,full_name,abbr_name,low_left_longtitude:double,up_right_longtitude:double,up_right_latitude:double,station_latitude:double,station,low_left_latitude:double,station_longtitude:double,:LABEL
610000000000,陕西省,197025.84,陕西省,陕西,105.4872,111.2422,39.58533,34.26645358,西安市新城区新城大院,31.70674,108.94952476,AD;Province
650000000000,新疆维吾尔自治区,1660000,新疆维吾尔自治区,新疆,73.49989,96.38728,49.18006,43.79179105,乌鲁木齐市中山路479号,34.33374,87.62484437,AD;Province
130000000000,河北省,187700,河北省,河北,113.4551,119.8485,42.61558,38.03705206,石家庄市长安区裕华东路113号,36.04881,114.52429283,AD;Province
...
最后的数据如上所示(省级部分)。直接看一坨CSV体验很差对吧,其实不用看全,看到主要的几个字段就是可以了,比如code:ID(AD)
,表示code
字段,行政代码,ID
表示我要用于之后导入关系的主键,(AD)
表示这个ID不是全局的,是一个叫AD
的group,可以理解命名空间,详情请看官方文档。还有LABEL
字段,表示标签……废话…… area:double
表示用双精度,否则默认为字符串,你不希望数字都变成字符串吧。
构建行政区上下级关系
在这之前,必须要验证很多细节,比如真的所有数据都有上级这个属性嘛?所有数据都是完美的符合规则的嘛?果然不是。。。
甚至,数据还有一些错误的,比如有一条就是陕西省-嘉峪关市-市辖区
,为什么我发现他是错误的呢?我是陕西人嘛?我地理及格了嘛?NONONO!因为我想验证一下所有地名的全称,去掉最后一段,即上级行政区全称,是否存在。哈哈哈不存在的,结果就是没找到甘肃省-嘉峪关市-市辖区
这条数据,仿佛链条都断掉了。
于是我跑了一下元数据,看看嘉峪关市到底是哪里的,发现陕西和甘肃都有,见鬼了,去网上搜了一下,不存在的。要把这条数据的「陕西」替换为「甘肃」,不想动元数据,所以写在了清理数据的脚本中。
用 UP_AD_NAME 字段找上级
用几个字典,存下地名和代号,然后拿 UP_AD_NAME 去匹配上一级的字典。
我太天真了哈哈哈,测试一下有多少重名的,果然到了县级就没法看了,什么「西城区」,哪个城市都有。
所以干脆拿全名前缀来匹配
直接把所有行政区存到一个字典,全名为键,代号为值。找一个行政区的上级时,其实就是去掉最后一段。比如 江苏省-南京市-江宁区
的上级就是 江苏省-南京市
,我验证了一下是不是字典里都存在。果然又㕛叒叕有几个不听话的数据。
比如 新疆生产建设兵团-农四师-兵团七十六团--三连
最后居然两个-
,中间是空的,我发现有这样的 新疆生产建设兵团-农四师-兵团七十六团-兵团七十六团
数据,就尝试给他们都加上,但结果是有些又找不到了。所以我干脆删了,后果是某些村级直接属于县级,算了算了没毛病,毕竟数据没给全,至少这样没什么大问题。
最后!居然发现有相同全名的,无话可说,仔细观察可以发现,都是村级的,名字一样,代号却不同,只能认为是干扰数据,随缘选一个吧。
完成CSV
:START_ID(AD),:END_ID(AD),:TYPE
610000000000,000000000000,BELONGS_TO
650000000000,000000000000,BELONGS_TO
130000000000,000000000000,BELONGS_TO
660000000000,000000000000,BELONGS_TO
140000000000,000000000000,BELONGS_TO
520000000000,000000000000,BELONGS_TO
360000000000,000000000000,BELONGS_TO
630000000000,000000000000,BELONGS_TO
230000000000,000000000000,BELONGS_TO
...
最后就是生成这样的数据,再写个导入 neo4j 的脚本即可。
怎么玩 Neo4j
算了下一篇文章再写吧,累死了……