安全博客友链数据分析可视化

转自先知 https://xz.aliyun.com/t/37


最近工作中一直在研究数据分析,试图在传统安全上做一些新的尝试,分析过程中用到了Gephi,此工具号称为“数据可视化领域的Photoshop”,研究之余,突然想到以前写的一个用于爬取友情链接的小工具,然后发现爬取的数据信息正好可以用来练习使用Gephi,于是花了一点时间准备对友情链接进行数据可视化。

爬虫

想要分析数据,首先我们得有获取数据的方法,爬虫便可以很方便的获取我们想要的数据。

需求分析:
我们需要分析的数据为友情链接之间的引用关系,那么需求很明确了,我们可以将某一博客作为爬取起点,然后进行递归爬取。

我的递归思路如下图:

爬虫采用广度优先策略,进行递归爬取,递归停止条件可设置为链接数量已经达到一定数量或者人工查看数据是否已经偏离设定的“圈子”,在此例中,圈子设定为安全圈子,也就是说当数据中出现大量非安全圈内的博客,说明爬虫就可以停止递归了

核心代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package spider;

import org.javaweb.core.net.HttpRequest;
import org.javaweb.core.net.HttpResponse;
import utils.MysqlConnection;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Created by jeary on 2017/8/16.
*/
public class DomainsSpider {
/**
* 函数主入口
* @param args
*/
public static void main(String[] args) {
Map<String[], Boolean> urls = new HashMap<String[], Boolean>();
urls.put(new String[]{"http://jeary.org", "Jeary"}, false);
crawllLinks(urls);
}

/**
* 递归爬行链接
* @param urlMap
*/
public static void crawllLinks(Map<String[], Boolean> urlMap) {
Map<String[], Boolean> urls = new HashMap<String[], Boolean>();
String urlString = "";
String name = "";
for (Map.Entry<String[], Boolean> url : urlMap.entrySet()) {
urlString = url.getKey()[0];
name = url.getKey()[1];
System.out.println(urlString+"\t"+name);
int siteid = findID(urlString);
if(siteid == 0){
add(new Object[]{name,urlString});
siteid = findID(urlString);
}
System.out.println("新的父节点ID:"+siteid+"\t");
String body = getHtml(urlString);
if (body != null && !body.isEmpty()) {
Map<String, String> links = getLinks(body, urlString);
for (Map.Entry<String, String> linkMap : links.entrySet()) {
String link = linkMap.getKey();
String title = linkMap.getValue();
urls.put(new String[]{link,title},false);
System.out.println("\t"+link+"\t"+title);
int ref_id = findID(link);
if(ref_id == 0){
add(new Object[]{title,link});
ref_id = findID(link);
}
System.out.println("发现新关系,准备入库:"+siteid+" -- "+ref_id);
add_ref(new Object[]{siteid,ref_id});
}
System.out.println("");
}
}
crawllLinks(urls);
}
/**
* 发起GET请求获取响应
* @param url
* @return
*/
public static String getHtml(String url) {
String body = "";
try {
HttpRequest request = new HttpRequest();
request.url(url);
request.followRedirects(true);
request.userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/47.0.2526.73 Safari/537.36");
HttpResponse response = request.get();
body = response.body();
} catch (Exception e) {
e.printStackTrace();
}
return body;
}

/**
* 从响应内容中提取友情链接
*
* @param body
* @param excuDomain
* @return
*/
public static Map<String, String> getLinks(String body, String excuDomain) {
Map<String, String> links = new HashMap<String, String>();
String regexp = "<a[^>]*href=(\"([^\"]*)\"|\'([^\']*)\'|([^\\s>]*))[^>]*>(.*?)</a>";
try {
if (body != null && !body.isEmpty()) {
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(body);

while (matcher.find()) {
String link = matcher.group(2);
String title = matcher.group(5);

if (title.matches("<.*>.*")) {
title = title.replaceAll("<.*?>", "");
}
if (link != null) {
if (link.indexOf(excuDomain) < 0 && link.startsWith("http")) {
URL url = new URL(link);
link = url.getProtocol() + "://" + url.getHost();
boolean isBlack = excuDomainList(link);
if (isBlack == false) {
links.put(link, title);
}
}
}
}
}
} catch (Exception e) {
System.out.println("异常匹配:" + excuDomain + "\r\n" + body);
}
return links;
}

/**
* 添加新链接
* @param domains
*/
public static void add(Object[] domains) {
try {
String sql = "insert into sites(name,domain) value (?,?)";
MysqlConnection mysqlConnection = new MysqlConnection();
mysqlConnection.execUpdate(sql, domains);
} catch (Exception e) {
System.out.println("插入链接数据异常,异常信息:"+e.getLocalizedMessage());
}
}

/**
* 添加新关系
* @param rel
*/
public static void add_ref(Object[] rel) {
try {
String sql = "insert into rel_sites(site_id,rel_site_id) value (?,?)";
MysqlConnection mysqlConnection = new MysqlConnection();
mysqlConnection.execUpdate(sql,rel);
} catch (Exception e) {
System.out.println("插入关系数据异常,异常信息:"+e.getLocalizedMessage());
}
}

/**
* 请求前查询请求链接ID,
*
* @param domain
* @return
*/
public static int findID(String domain) {
int id = 0;
String sql = "select id from sites where domain = ?;";
if(domain.startsWith("http://www.")){
domain = domain.replaceAll("http\\:\\/\\/www\\.","http://");
sql = "select id from sites where domain like ?";
}
MysqlConnection mysqlConnection = new MysqlConnection();
List<Object> list = mysqlConnection.execGetList(sql, new Object[]{domain});
if (list != null && list.size() > 0) {
id = Integer.parseInt(list.get(0).toString());
}
return id;
}

/**
* 过滤掉政府、Git、Apple网站、Google、本地IP等等
*
* @param domain
* @return
*/
public static boolean excuDomainList(String domain) {
String[] black = {".gov.cn", "github.com", ".apple.com", "weibo.com", ".google.com", ".csdn.net", "" +
".admin5.com", ".51.la", ".docker.com", "127.0.0.1", ".emlog.net", ".facebook.com", ".qq.com", "" +
".dockerone.com",
".mongodb.com", ".kindsoft.net", "twitter.com", ".gitlab.com", ".twitter.com", "youtube.com"};
for (int i = 0; i < black.length; i++) {
String blackDomain = black[i];
if (domain.lastIndexOf(blackDomain) > 0) {
return true;
}
}
return false;
}
}

在爬取的过程中,我们不仅需要获取友链、链接名字,我们还需要对应的关系数据,因为实在爬取的过程中获取关系数据,所以这个地方有点绕,但是想通了还是挺简单的。

首先我们建立两张表用来存储链接数据和关系数据,分别为为sites表和ref_sites表
sites表如下:

ref_sites表:

sites表比较简单,这里不做累述。
ref_sites表为链接之间的关系表

列名 说明
id 自增长ID
site_id 站点ID
ref_site_id 友链站点ID

上图中,我的网站ID为1, chmodx的ID为2
由于我友链了chmodx,那么数据中的关系就表示为

1
2
id site_id ref_site_id
1 1 2

处理数据格式

由于时间关系,我并没有爬太多的链接,直接停止了程序,开始整理数据为Gephi支持的数据格式

在使用Gephi之前,我们需要了解两个简单的概念,nodes和edges,nodes为节点,每一行数据都为一个nodes,我们称之为“点”,点通常为一个独立的个体数据,edges则是用于描述点与点之间的关系,我们称之为“边”,通常表示为“源” -> “目标”,当我们拥有这些信息之后,处理成Gephi支持的格式,然后就可以导入进工具进行分析了

nodes处理之后为下图:

edges处理后为:

其中Type下的Directed表示“Source”和“Target”的关系为又向的,如描述无向关系,则填写为“undirected”,其中weight表示边宽,数值越大边的宽度越大
当准备好以上数据之后,我们就可以直接导入进工具了

导入数据进Gephi

打开Gephi后,我们新建一个工程,然后按照如下顺序进行导入点数据和边数据

PS:记得点和边要分别导入,并且不要选错了文件弄混了点与边文件~

数据导入完成后,我们切换到“图”窗口,应该能看到如下视图:

然后选择一个布局算法,如:ForceAtlas2

运行后如图,下面我们对视图做一些调整,让数据方便我们直接查看
由于数据过多会导致运行卡顿、数据难以查看,这里设置“滤波”将边小于4的人进行排除,展示效果如图:

通过数据可视化分析,我们可以轻松得到数据中的某些关联信息,如各个博客之间的引用关系,是否存在间接关系等等。说到这里突然想起“六度分隔理论”,百科解释如下:

一个数学领域的猜想,名为SixDegreesofSeparation,中文翻译包括以下几种:六度分隔理论或小世界理论等。理论指出:你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过六个中间人你就能够认识任何一个陌生人。这就是六度分割理论,也叫小世界理论。“六度分隔”说明了社会中普遍存在的“弱纽带”,但是却发挥着非常强大的作用。有很多人在找工作时会体会到这种弱纽带的效果。通过弱纽带人与人之间的距离变得非常“相近”。所谓“六度分隔”,用最简单的话描述就是:在人际脉络中,要结识任何一位陌生的朋友,这中间最多只要通过六个朋友就能达到目的。

也就是说通过这种分析方式,你可以轻松的找到是哪六个朋友能让你认识某一个你想认识的人。什么?你想认识我?我会对这六个人的请求返回403,hhhhhhh。