基於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元素可以看到:
知道對應的登入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進行分析:
將response中的html拷貝出來,通過查詢首次載入頁面中顯示的人名,定位到管理員和成員對應的html資訊如下(位於id為groupsMemberBrowser的Element中):
格式化文字後,再進行詳細的分析:
區分管理員和普通成員,以及獲取他們的id,可以通過下圖所示的字首規則識別出來:
大致程式碼如下:
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文字獲取到對應的成員資訊,但是我們知道群組中的成員是遠遠不止這幾個的,我們通過觸發網頁頁面中的載入更多可以看到,每次往下,都會發起請求獲取更多成員資訊:
那麼,這個請求更多資料的url從哪裡獲取呢?回頭看首次返回的html,可以看到:
獲取載入更多的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
很明顯的,我們可以通過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;
}
複製程式碼