爬虫实践-基于Jsoup爬取Facebook群组成员信息

基于Jsoup爬取Facebook群组成员信息

我们知道,类似今日头条、UC头条这类的App,其内容绝大部分是来源于爬虫抓取。我们可以使用很多语言来实现爬虫,C/C++、Java、Python、PHP、NodeJS等,常用的框架也有很多,像Python的Scrapy、NodeJS的cheerio、Java的Jsoup等等。本文将演示如何通过Jsoup实现Facebook模拟登录,爬取特定群组的成员信息,并导出Excel。

Keywords: Netbeans, JSwing, Jsoup, Apache POI , Jackson
Source Code: FacebookGrabber

1. Facebook模拟登录

想要爬取Facebook上面的群组成员信息,第一步需要先进行登录,并将登录成功后的cookie保存,之后每次请求的headers中都要带上该cookie用于用户识别。

访问https://www.facebook.com/login 通过chrome检查html元素可以看到:

Facebook登录页面

知道对应的登录url以及请求参数之后,现在我们通过Jsoup来构造登录请求以获取用户cookie信息。

这里,我写了一个基类,方便请求调用的同时,自动将每次请求获取到的cookie保存并附带到下一次的请求当中:

    private Connection getConnection(String url) {
        return Jsoup.connect(url)
                .timeout(TIMEOUT_CONNECTION)
                .userAgent(userAgent)
                .followRedirects(true)
                .ignoreContentType(true);
    }

    protected Document requestDocument(String url, String httpMethod, Map<String, String> data) throws Exception {
        Connection connection = getConnection(url);
        if (data != null && data.size() > 0) {
            connection.data(data);
        }
        if (cookies != null) {
            connection.cookies(cookies);
        }
        Document resultDocument = HTTP_POST.equalsIgnoreCase(httpMethod) ? connection.post() : connection.get();
        return resultDocument;
    }

    protected Response requestBody(String url, String httpMethod, Map<String, String> data) throws Exception {
        Connection connection = getConnection(url);
        if (data != null && data.size() > 0) {
            connection.data(data);
        }
        if (cookies != null) {
            connection.cookies(cookies);
        }
        connection.method(HTTP_POST.equalsIgnoreCase(httpMethod) ? Connection.Method.POST : Connection.Method.GET);
        Connection.Response res = connection.execute();
        if (res.cookies() != null && !res.cookies().isEmpty()) {
            cookies = res.cookies();
        }
        return res;
    }

基于封装好的请求基类,接下来实现 模拟登录 就变得更加简单了:

    public FbUserInfo login(String email, String pass) throws Exception {
        // Urls.LOGIN = "https://www.facebook.com/login.php?login_attempt=1";
        Response initResponse = requestBody(Urls.LOGIN, HTTP_GET, null); //fetching cookie and saving

        Map<String, String> loginParams = new HashMap<>();
        loginParams.put("email", email);
        loginParams.put("pass", pass);
        Response loginResponse = requestBody(Urls.LOGIN, HTTP_POST, loginParams);
        Document loginDoc = loginResponse.parse();

        FbUserInfo userInfo = null;
        String userId = loginResponse.cookies().get("c_user"); // current login userId
        if (userId != null && userId.length() > 0) {
            userInfo = new FbUserInfo();
            userInfo.setId(userId);
        }
        return userInfo;
    }

2. 获取群组中的管理员以及成员列表

下面将以 Homesteads.and.Sustainability 为例,演示如何获取对应群组中的管理员以及成员列表。

访问https://www.facebook.com/groups/Homesteads.and.Sustainability/members/
,通过查看chrome的network中的请求和返回的response进行分析:

members_response

将response中的html拷贝出来,通过查找首次加载页面中显示的人名,定位到管理员和成员对应的html信息如下(位于id为groupsMemberBrowser的Element中):


members_html

格式化文本后,再进行详细的分析:
区分管理员和普通成员,以及获取他们的id,可以通过下图所示的前缀规则识别出来:


admins_members

大致代码如下:

    private List<FbGroupUserInfo> getMembers(Element groupsMembersElement, int role) {
        String prefix = role == Constants.GROUP_ROLE_ADMIN ? "admins_moderators_" : "recently_joined_";
        List<FbGroupUserInfo> userInfoList = new ArrayList<>();
        Elements memberElements = groupsMembersElement.select(String.format("div[id^=%s]", prefix));
        if (memberElements != null && memberElements.size() > 0) {
            for (Element e : memberElements) {
                String id = e.attr("id").replace(prefix, "");
                String nickName = e.select("a img").first().attr("aria-label");
                String userName = e.select("a").first().attr("href").split("\\?")[0];
                userName = userName.substring(userName.lastIndexOf("/"));
                Elements joinElements = e.getElementsByClass("_60rj");
                String joinDate = joinElements.size() > 0 && role == Constants.GROUP_ROLE_GENERAL
                        ? joinElements.first().text().trim() : "";
                FbGroupUserInfo userInfo = new FbGroupUserInfo();
                userInfo.setId(id);
                userInfo.setNickName(nickName);
                userInfo.setUserName(userName);
                userInfo.setJoinInfo(joinDate);
                userInfo.setRole(role);
                userInfoList.add(userInfo);
            }
        }
        return userInfoList;
    }

上面我们只是通过解析首次访问的html文本获取到对应的成员信息,但是我们知道群组中的成员是远远不止这几个的,我们通过触发网页页面中的加载更多可以看到,每次往下,都会发起请求获取更多成员信息:


request_more

那么,这个请求更多数据的url从哪里获取呢?回头看首次返回的html,可以看到:


more_url

获取加载更多的url代码大致如下:

    private String getMoreItemUrl(Element groupsMembersElement, int role) {
        String prefix = role == Constants.GROUP_ROLE_ADMIN ? "group_admins_moderators" : "group_confirmed_members";
        String moreItemUrl = "";
        try {
            moreItemUrl = groupsMembersElement.select(String.format("a[href^=/ajax/browser/list/%s/]", prefix))
                    .first().attr("href");
        } catch (Exception e) {
        }
        return moreItemUrl;
    }

第一次我们通过分析首次的html可以知道第一批成员以及第一个加载更多的url,那么接下来第二次以及之后每次返回的都是json数据了。同样的,通过分析返回的json格式,可以看到,json中的成员信息也是以html的文本返回的,依葫芦画瓢,不断循环直到没有加载更多的url,这样就可以获取到所有成员的id和nickname了。

获取ajax返回数据中的groupMembersElement,大体代码如下

 ajaxString = ajaxString.substring(ajaxString.indexOf("{"));
 ObjectMapper m = new ObjectMapper();
 JsonNode rootNode = m.readValue(ajaxString, JsonNode.class);
 String html = rootNode.get("domops").get(0).get(3).get("__html").getValueAsText();
 Element groupMembersElement = Jsoup.parse(html, "", Parser.xmlParser());

但是,只是获取到成员的id和nickname还不够,我们需要获取到成员更详细的信息:故乡、居住地、性别等。

3.获取用户详细信息

下面以 own a salon 这个Facebook用户为例,演示如何获取用户详细信息。

这里通过访问手机版页面(手机版页面获取信息更方便)https://m.facebook.com/profile.php?v=info&id=100005502877898

profile mobile site

profile current city

很明显的,我们可以通过html中的关键字来获取对应的故乡、居住地、性别等。大致代码如下:

    public FbUserInfo getUserInfo(String userId) throws Exception {
        // Urls.USER_PROFILE = https://m.facebook.com/profile.php?v=info&id=%s;
        String url = String.format(Urls.USER_PROFILE, userId);
        Document doc = requestDocument(url, HTTP_GET, null);

        Elements userNameElements = doc.select("div[title=Facebook] div div");
        Elements genderElements = doc.select("div[title=Gender] div div");
        Elements hometownElements = doc.select("h4:contains(Home Town)");
        Elements locationElements = doc.select("h4:contains(Current City)");

        String userName = userNameElements.size() > 0 ? userNameElements.first().text().trim() : "";
        String gender = genderElements.size() > 0 ? genderElements.first().text().trim() : "";
        String hometown = hometownElements.size() > 0 ? hometownElements.first().firstElementSibling().text().trim() : "";
        String location = locationElements.size() > 0 ? locationElements.first().firstElementSibling().text().trim() : "";

        FbUserInfo userInfo = new FbUserInfo();
        userInfo.setId(userId);
        userInfo.setUserName(userName);
        userInfo.setGender(gender);
        userInfo.setHometown(hometown);
        userInfo.setLocation(location);
        return userInfo;
    }

4.最终效果

主界面及导出数据
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,500评论 25 707
  • HTTP cookie(也称为web cookie,网络cookie,浏览器cookie或者简称cookie)是网...
    留七七阅读 17,832评论 2 71
  • 因为清王朝保护龙脉圣地、严禁开发的缘故,很长一段时间内,我都以为东北是未开化的蛮荒之地,是文化的荒漠。尤其对天寒地...
    渝夫2016阅读 273评论 0 2
  • 亲子日记第四十四天,今天是周末,上午陪蕙钰复习了一会功课准备后天的考试,中午朋友过生日,我们一家四口去给朋...
    AA稳稳阅读 181评论 0 0
  • 睫毛上的薄霜 遮不住 铺展开来的烂漫 率性毫无遮拦 翻山越岭 依山逐水 远处和不远处 色彩斑斓 层次分明 蜿蜒缓流...
    遁心阅读 123评论 0 0