个人博客:http://coolgo-zq.appspot.com/
前段时间,忽然想到做一个关于“六度空间”的应用-找出两个不认识人的联系。在自己经过一些准备以后,于是,故事开始了。
考虑到数据的敏感性,就不提供代码和数据的下载。在这里探讨一下,抛砖引玉,欢迎大家积极讨论。
“六度空间”理论:你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过六个人你就能够认识任何一个陌生人。
我们来假设一种场景:Z希望认识某个人X,但是到底采用什么方式去接近X呢。现在如果Z的好友F认识X的话,那么你可以通过F的关系,和X取得了正大光明的交往机会,然后从此一段美好的姻缘就此展开。(三者的关系:Z-->F-->X)
最初,我随意的看了一下几个sns的api,发现大多是对一个用户的好友列表有比较严格的控制,因此如果通过api的访问好友关系的话,会有个很大的弊端:需要用户授权。这样的话,在数据量很小的情况下,基本上就是个废品。这个时候,我又一次深刻的感受到了数据的重要性。
后来,发现rr网可以通过一种方式(下文详细展开)获得好友列表,因此就有了想法,想法之后就有了行动。
首先,要面对的第一个问题就是:在假设已经有了数据的情况下,如果找出两个不认识人之间的关系。先引入2个人X,Y。稍微分析一下,就会发现,好友关系图展开的话就是一张无向图。因此X,Y之间的关系问题就转化为:无向图中找出XY之间的最短路径。于是就考虑到了双向搜索,从X,Y两个人开始进行搜索。找出X的好友的集合SetX,同时找出Y的好友集合setY。setX,setY是否有交集,如果有的话,说明找出了他们之间的联系。如果没有,那么在把找出setX里面的人的好友,即X的好友的好友,同样找出setY。以此类推,直到找到发现两个存在交集。那么这些交集里面的人就是关键性人物了。找到这些人,XY的关系自然也就出来了。
以上是从算法方面考虑,本身并不复杂。
关于数据规模
假设每个人有100个好友,那么每当把好友关系拓展一次,就上升2个数量级,这种指数级的增长太可怕。
因此必须对好友的范围进行控制,于是我打算把好友范围控制在自己的学校。我粗略的估计了一下,我们一届算是4k个人,然后算上研究生博士生,那么也大概在1w吧,然后在算上rr网的年龄,我推算rr网上我们学校的注册用户数可能在8w左右。8w的数据规模虽然不小,但是还是可以接受的。
进过我自己的测试:基本上下载量与有效数据的比例是:400:1,单个人(以200个好友来算)的数据量为2kb~以8万人来计算:有效数据量160mb,下载量达到了64gb。这个下载量还是挺恐怖的。但是还是硬着头皮做了下去。
最后从爬虫爬下来的数据来看,每个人在校内平均大概100个好友,总体人数在4.5w左右。有效数据在30mb左右。
接下来,就是具体的工作:
1.如何实现模拟登录?
2.如何获取好友列表?
3.如何本地存储个人信息?
1.模拟登录:
通过对登陆协议的分析,有抓包工具抓到了post信息,然后发现rr网的登录模拟还是比较简单的。抓包的时候发现一个挺不错的工具,HttpAnalyzer,这个比wireshark好用一点。
在rr网登录过程中,只返回四个有效信息:email,password,origURL,domain。而且还是明文传输的。因此只要简单写一个登录方法,把这些值传过去就自然登录成功了。
2.获得好友列表:
http://friend.renren.com/GetFriendList.do?curpage=【页码】&id=【你需要找的id】
这个页码会返回一个此id的好友列表的页面,然后对这个页面提取有效信息就行了。
这是我的正则表达式
String regex = "<p class=\"avatar\"><a href=\"http://www.renren.com/profile.do\\?id=[0-9]+\"><img src=\"http://.+\" alt=\"http://.+\" /></a></p>";
值得一提的是,注意一下img 的src,里面的url的前缀不是每一个都相同的。我估计是rr网在升级的时候,一部分用户的头像还是取自老服务器,新用户在新服务器。
3.本地存储:
我使用了json格式来储存,使用的是gson包,操作起来比较简单。为了减少本地信息量,我存储了四个信息:id,name,imgUrl,school,friends。
public class Person {
private String id;
private String name;
private String school;
private String imgUrl;
private List<String> friends;
}
然后把所有的person放到了一个hashMap<id,Person>里面了。这样方便通过id直接找到person对象。
在最初阶段,我以为数据存储会比较大,所以没有考虑存到同一个文件中。而是每个person存放到了一个文件,这样下来的话,一共有4w+个小文件,每个文件只有1k左右。
缺点:当进行查询的关系的时候,IO资源太可怕,每找一个好友就要进行一次IO,当进行双向搜索的时候,这样磁盘扛不住啊这样很蛋疼。
后来参考了Create Chen的做法,把所有的都存放到了同一个文件中。于是就像把所有的person在程序刚启动时就load到内存的hashMap<id,Person>中。
import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Scanner; import java.util.Vector; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; public class FFinder { public Gson gson = new Gson(); private Map<String, Person> map; private int[] step = new int[50000]; /** 记录v[i]记录的是到达person[i]的前一个节点。 */ private Vector<Vector<String>> from = new Vector<Vector<String>>(); private Queue<String> q1 = new LinkedList<String>(); private Queue<String> q2 = new LinkedList<String>(); /** 将person.id映射到step数值中 */ private Map<String, String> idmap = new HashMap<String, String>(); public void getRelation(String id1, String id2) throws IOException { System.out.println(map.get(id1).getName()+"-->"+map.get(id2).getName()); init(); q1.add(id1); from.add(new Vector<String>()); from.get(0).add(id1); idmap.put(id1, String.valueOf(idmap.size())); step[0] = 1; q2.add(id2); from.get(1).add(id2); idmap.put(id2, String.valueOf(idmap.size())); step[1] = -1; // 是否交集 boolean isMeet = false; Queue<String> join = new LinkedList<String>(); while (!q1.isEmpty() && !q2.isEmpty()) { Queue<String> nextQ = new LinkedList<String>(); isMeet = deepFindHead(join, nextQ); if (isMeet) break; q1 = nextQ; nextQ = null; nextQ = new LinkedList<String>(); isMeet = deepFinderTail(join, nextQ); q2 = nextQ; } System.out.println("join=" + gson.toJson(join)); } private void init() { step = new int[50000]; from = new Vector<Vector<String>>(); q1 = new LinkedList<String>(); q2 = new LinkedList<String>(); idmap = new HashMap<String, String>(); /** * @param join * @param nextQ * @return */ private boolean deepFinderTail(Queue<String> join, Queue<String> nextQ) { String cur; Person p; int curIdnum; while (!q2.isEmpty()) { cur = q2.poll(); curIdnum = Integer.parseInt(idmap.get(cur)); p = map.get(cur); List<String> friends = p.getFriends(); for (String id : friends) { int idnum; if (idmap.containsKey(id)) { idnum = Integer.parseInt(idmap.get(id)); /** * 说明已经到达过此人 如果step[idnum]为-,则说明是从id2出来的,continue; * 如果为+,则说明找到了双向搜索出现了交集。 */ if (step[idnum] < 0) continue; else if (step[idnum] > 0) { // 找到交集 isMeet = true; System.out.println("="+step[curIdnum]+",通过"+(-2*step[curIdnum]-1)); from.get(idnum).add(cur); join.add(id); } } idmap.put(id, String.valueOf(idmap.size())); idnum = idmap.size() - 1; step[idnum] = step[curIdnum] - 1; from.add(new Vector<String>()); from.get(idnum).add(cur); if (!isMeet) nextQ.add(id); } return isMeet; private boolean deepFindHead(Queue<String> join, Queue<String> nextQ) { while (!q1.isEmpty()) { cur = q1.poll(); * 说明已经到达过此人 如果step[idnum]为正,则说明是从id1出来的,continue; * 如果为负数,则说明找到了双向搜索出现了交集。 if (step[idnum] > 0) else if (step[idnum] < 0) { System.out.println("="+step[curIdnum]+",通过"+(2*step[curIdnum]-2)); step[idnum] = step[curIdnum] + 1; public Map<String, Person> loadData(String path) throws IOException { File f = new File(path); BufferedReader in = new BufferedReader(new FileReader(f)); Map<String, Person> t = gson.fromJson(in.readLine(), new TypeToken<Map<String, Person>>() { }.getType()); in.close(); this.map = t; System.out.println("load ok!"); return t; public static void main(String[] argv) throws IOException { Scanner scan = new Scanner(System.in); FFinder f = new FFinder(); f.loadData("D:/1/allData_save.data"); String id1, id2; while (true) { id1 = scan.next(); id2 = scan.next(); System.out.println(id1 + "-->" + id2); f.getRelation(id1, id2); }