開源中國用戶分析

加入開源中國也有超過三年的時間了,覺得開源中國已經越辦越好了,突然很想知道它究竟有多好,我是不是開源中國最老的用戶,我有176個開源中國的積分能夠排名第幾,帶着這些問題,我抓取了部分開源中國的用戶信息,做了一個簡單的分析。

數據獲取

要獲得用戶數據,可以通過開源中國的網頁來進行。這個是我的主頁面

這個頁面包含了用戶的基本信息,包括用戶名,積分,粉絲,關注等等。

點擊粉絲鏈接可以獲得所有的粉絲的情況

然後我們就可以通過這些鏈接,迭代的找到所有相關連的數據了。

工具選取

這次的數據抓取我選用了requestspyquery

requests是一個非常好用的python的http/rest的客戶端,比之python自帶的urllib要好用很多,推薦使用。

pyquery是用來解析和操縱html和DOM文檔,提供類似jquery的語法,比諸如beatifulSoap要好用不少,尤其如果你是一個前段開發者,熟悉jquery,那就更方便了。大家可以參考我的另一篇博客瞭解更多的背景信息。

爬取網頁數據

爲了抓取網頁的內容,我們可用chrome自帶的工具來查看網頁的DOM結構:

核心的代碼如下:

def get_user_info(url):
    try:
        r = requests.get(url + "/fans", headers=headers)
        doc = pq(r.text)
        user_info = dict()

        # get user information
        user_info["url"] = url
        user_info["nickname"] = doc.find(".user-info .nickname").text()
        user_info["post"] = doc.find(".user-info .post").text()
        user_info["address"] = doc.find(".user-info .address").text()
        user_info["score"] = doc.find(".integral .score-num").text()
        user_info["fans_number"] = doc.find(".fans .score-num").text()
        user_info["follow_number"] = doc.find(".follow .score-num").text()
        join_time = doc.find(".join-time").text()
        user_info["join_time"] = join_time[
            4:15].encode("ascii", "ignore").strip()

        # get fans
        user_info["fans"] = get_relations(doc)

        # get follows, fellow is a wrong spelling
        rf = requests.get(url + "/fellow", headers=headers)
        user_info["follow"] = get_relations(pq(rf.text))

        return user_info
    except Exception as e:
        return None

get_user_info() 方法通過給定的用戶url來獲取用戶信息,首先利用requests的get方法得到用戶網頁,然後用pyquery抽取出暱稱nickname,職位post,地址address,積分score,粉絲數fans_number,關注數follow_number。並得到所有的關注和粉絲的url。這裏有一件事比較尷尬,關注的url模式中,關注的英文單詞拼錯了,應該是follow,而實際上使用的卻是fellow,前端程序員也要學好英語呀!

def get_relations(basedoc):
    result = list()
    try:
        doc = basedoc
        fans = doc.find(".fans-item")
        flist = [fans.eq(i) for i in range(len(fans))]

        while True:
            for fan in flist:
                username = fan.find(".username").text()
                url = fan.find(".username a").attr("href")
                result.append({"name": username, "link": url})

            pages = doc.find("#friend-page-pjax a")

            if len(pages) == 0:
                break

            last_link = pages.eq(len(pages) - 1).attr("href")
            last_number = pages.eq(len(pages) - 1).text()

            if last_number.encode("utf-8").isdigit():
                break

            r = requests.get(BASE_URL + "/" + last_link, headers=headers)
            doc = pq(r.text)
            fans = doc.find(".fans-item")
            flist = [fans.eq(i) for i in range(len(fans))]

        return result
    except Exception as e:
        return result

get_relations()方法通過循環的方式找到所有的關注或粉絲的url鏈接。

最後實現一個抓數據的類Scraper:

class Scarper(threading.Thread):
    def __init__(self, threadName, queue):
        super(Scarper, self).__init__(name=threadName)
        self._stop = False

        self._base_url = BASE_URL
        self._base_user = "masokol"
        self._check_point = dict()
        self._task_queue = queue

    def _pull(self, url):
        user = get_user_info(url)
        if user is None:
            return

        self._write(user)
        for u in user["fans"]:
            logger.debug("check a fan {}".format(json.dumps(u)))
            if not self._check_point.has_key(u["link"]):
                logger.debug("put one task {}".format(u["link"]))
                self._task_queue.put(u)
        for u in user["follow"]:
            logger.debug("check a follow {}".format(json.dumps(u)))
            if not self._check_point.has_key(u["link"]):
                logger.debug("put one task {}".format(u["link"]))
                self._task_queue.put(u)

    def _write(self, user):
        if self._check_point.has_key(user["url"]):
            return
        logger.debug(json.dumps(user))

        # TODO support unicode logging here
        logger.info("name={}, join={}, post={}, address={}, score={}, fans_number={}, follow_number={}".format(
            user["nickname"].encode("utf8"),
            user["join_time"],
            user["post"].encode("utf8"),
            user["address"].encode("utf8"),
            user["score"].encode("utf8"),
            user["fans_number"].encode("utf8"),
            user["follow_number"].encode("utf8")))
        self._check_point[user["url"]] = True

    def init(self):
        url = self._base_url + "/" + self._base_user
        r = requests.get(url, headers=headers)
        self._pull(url)

    def run(self):
        global IS_TERM
        logger.debug("start working")

        try:
            while True:
                logger.debug("pull one task ...")
                item = self._task_queue.get(False)
                logger.debug("get one task {} ".format(json.dumps(item)))
                self._pull(item["link"])
                time.sleep(0.1)
                if IS_TERM:
                    break
        except KeyboardInterrupt:
            sys.exit()
        except Exception as e:
            print e

這裏面有幾個點要注意下:

  • 起始用戶我選用了“masokol”,理論上選哪個用戶作爲起始用戶關係不大,假定所有的用戶都是關聯的,當然這個假定不一定成立。也許存在一個孤島,孤島裏的用戶和其它用戶都不關聯。如果起始用戶在這個孤島裏,那麼就只能抓取顧島內的用戶。
  • 利用隊列構造生產者消費者模型,這樣做的好處是可以利用多線程來提高抓取效率,但是實際操作中,我並沒有開多線程,因爲不希望給oschina帶來太多的網絡負載。大家如果要運行我的程序也請注意,友好使用,不要編程ddos攻擊。另外利用queue可以把遞歸調用變爲循環,避免stack overflow
  • checkpoint用來記錄抓取的歷史,避免重複抓數據。這裏是用url作爲key的一個數據字典dict來做checkpoint

完整的代碼請參考 https://github.com/gangtao/oschina_user_analysis 

這裏是一個數據抓取程序的最簡單實現,若要做到真正好用,有以下幾點需要考慮:

  • 持久化任務隊列
    在該實現中,任務隊列在內存中,這樣就很難做分佈式的擴展,也沒辦法做到錯誤恢復,如果程序或數據抓取的節點出問題,則抓取中斷。可以考慮使用Kafka,RabbitMQ,SQS,Kenisis來實現這個任務隊列
  • 持久化checkpoint
    同樣的內存內的checkpoint雖然效率高可是無法從錯誤中恢復,而且如果數據量大,還存在內存不夠用的情況
  • 提高併發
    在本實現中,並沒有利用併發來提高數據吞吐,主要是不想給oschina帶來高的負載。通過配置多線程/多進程等手段,可以有效的提高數據抓取的吞吐量。
  • 優化異常處理
    在該實現中,大部分錯誤都沒有處理,直接返回。
  • 利用雲和無服務
    利用雲或者無服務(serverless,例如AWS Lamba)技術可以把數據採集服務化,並且可以做到高效的擴展。

數據分析

利用Splunk可以高效的分析日誌文件,我在我的Splunk中創建了一個文件的Monitor,這樣就可以一邊抓取數據,一邊分析啦。

這裏關鍵的設置是sourcetype=oschina_user

好了上分析結果:

以上幾個指標是已分析的用戶數,平均積分,平均粉絲數和平均關注數。我的176分和105個粉絲都剛好超過平均值一點點呀。

下面是對應的SPL(Splunk Processing Language),因爲日誌輸出是使用的key=value格式,不需要額外抽取,直接可以用SPL來分析。

sourcetype=oschina_user | stats count
sourcetype=oschina_user | stats avg(score)
sourcetype=oschina_user | stats avg(fans_number)
sourcetype=oschina_user | stats avg(follow_number)


sourcetype=oschina_user | table name, score | sort -score | head 10

這張表是最高積分榜,紅薯兩萬多分是自己改得數據庫吧,太不像話了。另外幾位大神,jfinal是量子通信的技術副總裁;南湖船老大失業中,大概不想透漏工作信息吧,小小編輯就不說了,如夢技術是皕傑 - 後端工程師。

sourcetype=oschina_user | stats count by addr0 | sort -count | head 5

(注意:這裏的addr0字段需要額外的從地址字段中抽取出來,因爲原字段包含省份/地區兩級信息)

用戶來自哪裏,北廣上佔據前三不意外,上海排第三要加油呦。

sourcetype=oschina_user | table name, fans_number |sort - fans_number | head 5
sourcetype=oschina_user | stats count by post | sort -count | head 5
sourcetype=oschina_user | table name, follow_number | sort -follow_number | head 5

擁有最多粉絲和關注數的的信息。Post是職業信息,都是程序員毫無新意。

sourcetype=oschina_user | stats count by join_year | sort join_year

(注意:這裏的join_year字段需要額外的從join字段抽取出來。)

這個厲害了,是用戶加入oschina的趨勢圖,2014年以後咋是下降的呢,難道是發展的不好?我是不是發現了什麼?紅薯要加油了呀!也許是我抓取的用戶數量還太少,不能反應真實情況吧。

我的爬蟲還在慢慢跑,有了最新的發現會告訴大家!

後記

最新的數據更新到4800,後面不一定會繼續抓數據,大家看看就好。

  • Top Rank的排名變化不大
  • 平局積分下降到38.5,平均粉絲數下降到21,看來我開始抓取的用戶都是核心用戶,積分高,粉絲多
  • 這個“我的名字叫李猜”關注了13萬用戶,你想幹啥?
  • 最新的數據現實開源中國用戶數量增長顯著,17年這還不到一半,新用戶的數量明顯超過了2016,恭喜紅薯了
  • 還有很多可以抓去的信息,例如用戶性別,發表的博客數量等等,留個大家去擴展吧。
  • 應紅薯的要求,代碼也放到了碼雲上 http://git.oschina.net/gangtao/oschina_user_analysis 
  • Splunk真的是太好用了(此處應有掌聲)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章