Java 知乎爬蟲

目標(biāo)

爬取知乎用戶信息刁标,并作簡要分析博助。所爬的對象是關(guān)注者≥10的用戶,因為:

  1. 關(guān)注者數(shù)量<10的用戶猪瞬,很多的僵尸用戶憎瘸、不活躍用戶
  2. 我爬蟲的目的也不是大而全,高質(zhì)量用戶更有分析意義

整體思路

JDK 環(huán)境

JDK 1.7

存儲結(jié)構(gòu):redis

為什么使用 redis陈瘦?

  1. 基于內(nèi)存的存儲幌甘,速度快,同時又具有持久性
  2. 開發(fā)非常簡單
  3. 多種數(shù)據(jù)結(jié)構(gòu)痊项,自帶排序功能
  4. 斷電锅风、異常時能保存結(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

點擊「關(guān)注者」眯勾,url 變成了:https://www.zhihu.com/people/warfalcon/followers,界面是這樣的:

而點擊「關(guān)注了」婆誓,url 變成了:https://www.zhihu.com/people/warfalcon/following吃环,界面是這樣的:

通過對比上面的3個 url,我們發(fā)現(xiàn)結(jié)構(gòu)可能是下面這樣的:

  1. https://www.zhihu.com是域名
  2. /people 代表是個人賬號洋幻,美團(tuán)的知乎賬號是這樣的:https://www.zhihu.com/org/mei-tuan-dian-ping-ji-shu-tuan-dui/activities郁轻,發(fā)現(xiàn) /org 是企業(yè)賬號
  3. 接下來的warfalcon是用戶的唯一標(biāo)識,和用戶顯示的名稱是不一樣的
  4. /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)是下面這樣的:

但是爬蟲出來的結(jié)果是沒有這個div的躲查,在整個 response 中搜索「大頭幫主」它浅,會發(fā)現(xiàn)存在于//div[@id='data']/@data-state結(jié)構(gòu)中,將其所有的 &quot;都替換成引號镣煮,就可以發(fā)現(xiàn)下面的 json 結(jié)構(gòu):

發(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ù):

發(fā)現(xiàn)只有在訪問對應(yīng)的 urlToken 的用戶時蓖柔,才有教育程度、居住地點等信息风纠,測試其它賬號也是一樣的(另况鸣,還有一個返回比較全的信息是個人信息)。

爬蟲分頁

該用戶關(guān)注了610人竹观,每頁顯示20人懒闷,正好需要31頁。

發(fā)現(xiàn)第2頁的 url 是:https://www.zhihu.com/people/warfalcon/following?page=2栈幸,只需要在原來的網(wǎng)址上加上參數(shù) page 即可愤估。

策略分析

我們需要爬取一個用戶所關(guān)注的所有用戶嗎?我覺得并不需要速址。因為:

  1. 單個用戶可能關(guān)注了1000人玩焰,且有1000人關(guān)注了他。這是一個復(fù)雜的網(wǎng)絡(luò)芍锚,我覺得取用戶關(guān)注的前兩頁(即40人)昔园,就足夠了蔓榄。
  2. 按照上面的分析,也沒有必要將關(guān)注了他的用戶放入待爬蟲的列表默刚。
  3. 僅followerCount>10的用戶甥郑,才加入待爬蟲列表。
  4. 僅在訪問對應(yīng)的urlToken時荤西,才會將這個用戶的信息存入redis中(因為僅此時才有教育信息澜搅、地點信息)。
  5. 如果redis中已經(jīng)有了這個人的信息邪锌,則將其排除掉勉躺,也不要將其關(guān)注者放入待爬蟲列表,否則會導(dǎo)致非常巨大的冗余觅丰,爬了一些人之后就會非常慢

分析爬蟲結(jié)果

代碼貼在文章結(jié)尾處(很短饵溅,核心就50行左右)。先分析下爬蟲結(jié)果(僅爬到了3w數(shù)據(jù)妇萄,第一次想分析數(shù)據(jù)時蜕企,誤刪了所有爬蟲數(shù)據(jù)……現(xiàn)在又爬了一遍,寫博客的時候才爬到3w冠句,就這樣吧~)轻掩,「粉絲用戶最多的用戶」、「回答數(shù)最多的用戶」就不分析了轩端。

知乎用戶高校排名

城市排名

代碼

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é)

  1. 爬蟲結(jié)束后,想把 redis 數(shù)據(jù)從一臺電腦轉(zhuǎn)移到另一臺電腦氧卧,小手一抖就給刪除了……浪費了很長時間
  2. 僅開3個線程桃笙,是不需要代理IP的;爬取時也不需要隨機(jī)休眠一段時間
  3. redis 存儲用戶信息使用的 json 格式沙绝,可能有些大搏明。但是想想一個用戶大概170字節(jié)鼠锈,3w用戶也就不到10M。
  4. 線程池星著、超時重試什么的都沒管购笆,都是 webmagic 框架做的
  5. 通過分析發(fā)現(xiàn),知乎用戶都是清北的虚循,而且除了北上廣深同欠,居住在國外的用戶也能占據(jù)30%
  6. 數(shù)據(jù)不準(zhǔn)確,所爬的對象是關(guān)注者≥10的用戶
  7. 學(xué)校邮丰、居住地的分析并不嚴(yán)謹(jǐn)行您,因為地點北京市海淀區(qū)并沒有包括在北京中,學(xué)校也同理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剪廉,一起剝皮案震驚了整個濱河市娃循,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斗蒋,老刑警劉巖捌斧,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泉沾,居然都是意外死亡捞蚂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門跷究,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姓迅,“玉大人,你說我怎么就攤上這事俊马《〈妫” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵柴我,是天一觀的道長解寝。 經(jīng)常有香客問我,道長艘儒,這世上最難降的妖魔是什么聋伦? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮界睁,結(jié)果婚禮上觉增,老公的妹妹穿的比我還像新娘。我一直安慰自己翻斟,他們只是感情好抑片,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杨赤,像睡著了一般敞斋。 火紅的嫁衣襯著肌膚如雪截汪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天植捎,我揣著相機(jī)與錄音衙解,去河邊找鬼。 笑死焰枢,一個胖子當(dāng)著我的面吹牛蚓峦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播济锄,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼暑椰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了荐绝?” 一聲冷哼從身側(cè)響起一汽,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎低滩,沒想到半個月后召夹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡恕沫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年监憎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婶溯。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鲸阔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迄委,到底是詐尸還是另有隱情褐筛,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布跑筝,位于F島的核電站死讹,受9級特大地震影響瞒滴,放射性物質(zhì)發(fā)生泄漏曲梗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一妓忍、第九天 我趴在偏房一處隱蔽的房頂上張望虏两。 院中可真熱鬧,春花似錦世剖、人聲如沸定罢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祖凫。三九已至琼蚯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惠况,已是汗流浹背遭庶。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留稠屠,地道東北人峦睡。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像权埠,于是被迫代替她去往敵國和親榨了。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 0.需要的知識點 正則表達(dá)式 java多線程線程池池知識 httpclient網(wǎng)絡(luò)庫及json和html結(jié)構(gòu) 1....
    關(guān)耳金名閱讀 8,593評論 1 21
  • 前言 很早就有采集知乎用戶數(shù)據(jù)的想法,要實現(xiàn)這個想法秩彤,需要寫一個網(wǎng)絡(luò)爬蟲(Web Spider)叔扼。因為在學(xué)習(xí) py...
    囈語_yiyu閱讀 6,720評論 12 148
  • 初戀,多么美好的一個字眼漫雷,相信大多數(shù)童鞋的初戀都是高中時期或者初中時期吧瓜富。很不巧,我真正意義上的初戀確實發(fā)生在25...
    nhwxx閱讀 293評論 0 0
  • 大家都知道獅子是萬獸之王,而老虎則是百獸之王蓄坏。雖然光從數(shù)字上來看好像差了不是一兩個等級价捧,但是這不過是因為獅子的鬃毛...
    寵寶記閱讀 332評論 0 0
  • 你已經(jīng)很久沒有聽過一首歌后想喝酒。 也可能是今夜沒控制好情緒涡戳。 那個不知名的男人用若無其事...
    Synthia閱讀 175評論 0 1