目標(biāo)
爬取知乎用戶信息刁标,并作簡要分析博助。所爬的對象是關(guān)注者≥10
的用戶,因為:
- 關(guān)注者數(shù)量<10的用戶猪瞬,很多的僵尸用戶憎瘸、不活躍用戶
- 我爬蟲的目的也不是大而全,高質(zhì)量用戶更有分析意義
整體思路
JDK 環(huán)境
JDK 1.7
存儲結(jié)構(gòu):redis
為什么使用 redis陈瘦?
- 基于內(nèi)存的存儲幌甘,速度快,同時又具有持久性
- 開發(fā)非常簡單
- 多種數(shù)據(jù)結(jié)構(gòu)痊项,自帶排序功能
- 斷電锅风、異常時能保存結(jié)果
爬蟲框架:webmagic
官方網(wǎng)站:http://webmagic.io/。
為什么使用 webmagic鞍泉?
基于 Java 的 webmagic遏弱,開發(fā)極其簡單,這個知乎爬蟲的代碼主體就幾行塞弊,而且只要專注提取數(shù)據(jù)就行了(其實是因為我也不知道其它 Java 的爬蟲框架)。
代理 IP
沒有使用代理 IP泪姨,經(jīng)測試開20個線程爬知乎會被封IP游沿,我就開了3個線程。
爬取速度
30小時爬取了3w用戶(關(guān)注者數(shù)量≥10的用戶)肮砾,確實慢了點(部分原因是知乎的網(wǎng)站結(jié)構(gòu)诀黍,下面分析)。
分析知乎的網(wǎng)站結(jié)構(gòu)
以一個我關(guān)注的知乎大佬為例仗处,url 是:https://www.zhihu.com/people/warfalcon/answers
![](http://static.zybuluo.com/Yano/ok126p5zzrixherjtdz5908p/image.png)
點擊「關(guān)注者」眯勾,url 變成了:https://www.zhihu.com/people/warfalcon/followers,界面是這樣的:
![](http://static.zybuluo.com/Yano/0qzyryu7ruuw0extv8vljboj/image.png)
而點擊「關(guān)注了」婆誓,url 變成了:https://www.zhihu.com/people/warfalcon/following吃环,界面是這樣的:
![](http://static.zybuluo.com/Yano/kzhd2lb5rav7fiv3q66krybv/image.png)
通過對比上面的3個 url,我們發(fā)現(xiàn)結(jié)構(gòu)可能是下面這樣的:
- https://www.zhihu.com是域名
- /people 代表是個人賬號洋幻,美團(tuán)的知乎賬號是這樣的:https://www.zhihu.com/org/mei-tuan-dian-ping-ji-shu-tuan-dui/activities郁轻,發(fā)現(xiàn) /org 是企業(yè)賬號
- 接下來的warfalcon是用戶的唯一標(biāo)識,和用戶顯示的名稱是不一樣的
- /answers是該用戶回答的問題文留;/followers是關(guān)注了他的人好唯;/following是他關(guān)注了的人。
而一般來說燥翅,一個用戶「關(guān)注了」的人骑篙,比關(guān)注了這個用戶的人更有價值:被關(guān)注的人更有可能是大V。對比上面的圖片森书,發(fā)現(xiàn)warfalcon關(guān)注的人的關(guān)注者都是上萬的靶端,而關(guān)注他的人——至少前三個——都是0關(guān)注者谎势。
確定爬蟲的規(guī)則
warfalcon 關(guān)注的列表第一個用戶是:大頭幫主,在https://www.zhihu.com/people/warfalcon/following這里看到的網(wǎng)頁結(jié)構(gòu)是下面這樣的:
![](http://static.zybuluo.com/Yano/pt9ws45welp3h2snj2r8t2px/image.png)
但是爬蟲出來的結(jié)果是沒有這個div的躲查,在整個 response 中搜索「大頭幫主」它浅,會發(fā)現(xiàn)存在于//div[@id='data']/@data-state
結(jié)構(gòu)中,將其所有的 "
都替換成引號镣煮,就可以發(fā)現(xiàn)下面的 json 結(jié)構(gòu):
![](http://static.zybuluo.com/Yano/2bc7xeclw8mkrc7l9os757a8/image.png)
發(fā)現(xiàn)這里的 name 是「大頭幫主」姐霍,其關(guān)注者數(shù)量和上面的截圖一致,確認(rèn)查找是正確的典唇。這個json的常用字段:
isFollowed:對方是否關(guān)注了自己(猜測)
userType:用戶類型镊折,有 用戶、企業(yè)等
answerCount:回答問題的數(shù)量
isFollowing:自己是否關(guān)注了對方(猜測)
urlToken:用戶的唯一標(biāo)識介衔,url中用的就是這個字段
id:用戶的id恨胚,唯一標(biāo)識,不利于記憶炎咖,所以才有上面的urlToken赃泡,應(yīng)該是一一對應(yīng)的
name:用戶的名稱,可以自定義乘盼,所以可以重復(fù)
gender:1是男升熊,0是女,-1表示未填寫
isOrg:是否為企業(yè)賬號绸栅,和上面的userType有一點冗余
followerCount:被關(guān)注者的數(shù)量
bedge:行業(yè)
但是這里缺少了一些信息:教育程度级野、居住地點呢?因為抓取的url是https://www.zhihu.com/people/warfalcon/following粹胯,分析他的json數(shù)據(jù):
![](http://static.zybuluo.com/Yano/segufxnrqa46b4iumhmayx3f/image.png)
發(fā)現(xiàn)只有在訪問對應(yīng)的 urlToken 的用戶時蓖柔,才有教育程度、居住地點等信息风纠,測試其它賬號也是一樣的(另况鸣,還有一個返回比較全的信息是個人信息)。
爬蟲分頁
該用戶關(guān)注了610人竹观,每頁顯示20人懒闷,正好需要31頁。
![](http://static.zybuluo.com/Yano/zzryy6lyhto3sbq04ok2naxj/image.png)
發(fā)現(xiàn)第2頁的 url 是:https://www.zhihu.com/people/warfalcon/following?page=2栈幸,只需要在原來的網(wǎng)址上加上參數(shù) page
即可愤估。
策略分析
我們需要爬取一個用戶所關(guān)注的所有用戶嗎?我覺得并不需要速址。因為:
- 單個用戶可能關(guān)注了1000人玩焰,且有1000人關(guān)注了他。這是一個復(fù)雜的網(wǎng)絡(luò)芍锚,我覺得取用戶關(guān)注的前兩頁(即40人)昔园,就足夠了蔓榄。
- 按照上面的分析,也沒有必要將關(guān)注了他的用戶放入待爬蟲的列表默刚。
- 僅followerCount>10的用戶甥郑,才加入待爬蟲列表。
- 僅在訪問對應(yīng)的urlToken時荤西,才會將這個用戶的信息存入redis中(因為僅此時才有教育信息澜搅、地點信息)。
- 如果redis中已經(jīng)有了這個人的信息邪锌,則將其排除掉勉躺,也不要將其關(guān)注者放入待爬蟲列表,否則會導(dǎo)致非常巨大的冗余觅丰,爬了一些人之后就會非常慢
分析爬蟲結(jié)果
代碼貼在文章結(jié)尾處(很短饵溅,核心就50行左右)。先分析下爬蟲結(jié)果(僅爬到了3w數(shù)據(jù)妇萄,第一次想分析數(shù)據(jù)時蜕企,誤刪了所有爬蟲數(shù)據(jù)……現(xiàn)在又爬了一遍,寫博客的時候才爬到3w冠句,就這樣吧~)轻掩,「粉絲用戶最多的用戶」、「回答數(shù)最多的用戶」就不分析了轩端。
知乎用戶高校排名
![](http://static.zybuluo.com/Yano/69t14tiskwrk5860kg9ojcvu/image.png)
城市排名
![](http://static.zybuluo.com/Yano/1fu5evmuw3zidwz7smfk9df6/image.png)
代碼
pom 文件
需要爬蟲框架 webmagic。
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
用戶信息類
僅列出字段逝变,get和set方法未列出基茵。
public class ZhihuUserDo {
private boolean org;
private String type;
private int answerCount;
private int articlesCount;
private String name;
private int gender;
private String urlToken;
private int followerCount;
private int followingCount;
private String edu; // 僅自己才有
private String loc; // 僅自己才有
核心爬蟲類
沒有啟動 web 服務(wù),直接寫的 main 函數(shù)運(yùn)行壳影。核心邏輯就是 process 函數(shù)拱层,如果不獲取第二頁數(shù)據(jù)會簡潔許多,對結(jié)果應(yīng)該也不會造成影響宴咧。
public class ZhihuUserProcessor implements PageProcessor {
private Site site = Site.me().setCycleRetryTimes(1).setRetryTimes(1).setSleepTime(200).setTimeOut(3 * 1000)
.setUserAgent(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.addHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3").setCharset("UTF-8");
private static Jedis jedis = RedisUtil.getJedis();
private static final String MAP_KEY = "zhihu_user";
private static final int THRES_HOLD = 10;
private static final int USERS_ONE_PAGE = 20;
@Override
public void process(Page page) {
String dataJson = page.getHtml().xpath("http://div[@id='data']/@data-state").all().get(0);
String urlString = page.getUrl().toString();
String urlToken = urlString.substring(START_LANGTH, urlString.lastIndexOf("/"));
JSONObject entities = (JSONObject) JSONObject.parseObject(dataJson).get("entities");
JSONObject users = entities.getJSONObject("users");
for (String key : users.keySet()) {
JSONObject object = users.getJSONObject(key);
ZhihuUserDo zhihuUserDo = JSONObject.parseObject(object.toString(), ZhihuUserDo.class);
/**
* 1. following 和 followers 都有自己的信息根灯,只需要用一個即可 2. 僅自己,僅有edu 和 loc 信息
*/
if (zhihuUserDo.getUrlToken().equals(urlToken) && !urlString.contains("?page=")) {
if (jedis.hexists(MAP_KEY, urlToken)) {
continue;
}
// educations
Object educations = object.get("educations");
if (educations != null) {
JSONObject school = (JSONObject) JSON.parseArray(educations.toString()).get(0);
if (school != null) {
zhihuUserDo.setEdu(((JSONObject) school.get("school")).getString("name"));
}
}
// locations
Object locations = object.get("locations");
if (locations != null) {
JSONObject loc = (JSONObject) JSON.parseArray(locations.toString()).get(0);
if (loc != null) {
zhihuUserDo.setLoc(loc.getString("name"));
}
}
// 「關(guān)注了」需要分頁掺栅,僅在本人信息中才有該字段
if (zhihuUserDo.getFollowingCount() > USERS_ONE_PAGE) {
int pagesTotal = zhihuUserDo.getFollowingCount() / USERS_ONE_PAGE + 1;
pagesTotal = Math.min(4, pagesTotal); // 防止「關(guān)注了」過多
List<String> urls = new ArrayList<>();
for (int i = 2; i <= pagesTotal; i++) {
urls.add(new StringBuilder(URL_START).append(urlToken).append(URL_FOLLOWING).append("?page=")
.append(i).toString());
}
page.addTargetRequests(urls);
}
jedis.hset(MAP_KEY, urlToken, JSON.toJSONString(zhihuUserDo));
} else {
// 如果被關(guān)注者>=10人烙肺,則加入爬蟲隊列
if (zhihuUserDo.getFollowerCount() >= THRES_HOLD
&& !jedis.hexists(MAP_KEY, zhihuUserDo.getUrlToken())) {
page.addTargetRequest(URL_START + zhihuUserDo.getUrlToken() + URL_FOLLOWING);
}
}
}
}
private static final String URL_START = "https://www.zhihu.com/people/";
private static final String URL_FOLLOWING = "/following";
private static final int START_LANGTH = URL_START.length();
public static void main(String[] args) {
start();
}
public static void start() {
List<String> urls = new ArrayList<>();
urls.add("https://www.zhihu.com/people/warfalcon/following");
urls.add("https://www.zhihu.com/people/warfalcon/followers");
Spider.create(new ZhihuUserProcessor()).addUrl(urls.get(0), urls.get(1)).thread(3).run();
}
@Override
public Site getSite() {
return site;
}
}
總結(jié)
- 爬蟲結(jié)束后,想把 redis 數(shù)據(jù)從一臺電腦轉(zhuǎn)移到另一臺電腦氧卧,小手一抖就給刪除了……浪費了很長時間
- 僅開3個線程桃笙,是不需要代理IP的;爬取時也不需要隨機(jī)休眠一段時間
- redis 存儲用戶信息使用的 json 格式沙绝,可能有些大搏明。但是想想一個用戶大概170字節(jié)鼠锈,3w用戶也就不到10M。
- 線程池星著、超時重試什么的都沒管购笆,都是 webmagic 框架做的
- 通過分析發(fā)現(xiàn),知乎用戶都是清北的虚循,而且除了北上廣深同欠,居住在國外的用戶也能占據(jù)30%
- 數(shù)據(jù)不準(zhǔn)確,所爬的對象是
關(guān)注者≥10
的用戶 - 學(xué)校邮丰、居住地的分析并不嚴(yán)謹(jǐn)行您,因為地點
北京市海淀區(qū)
并沒有包括在北京
中,學(xué)校也同理