爬蟲實戰(一):爬取微博使用者資訊

ACool發表於2018-07-15

前敘

系列文章:

爬蟲實戰(一):爬取微博使用者資訊

爬蟲實戰(二):Selenium 模擬登入並爬取資訊

爬蟲實戰(三):微博使用者資訊分析

該系列文章介紹了什麼?

1.爬蟲分析和處理方法

2.Python中的資料庫操作方法

3.Selenium瀏覽器自動化以及無頭瀏覽器使用方法

4.對資料進行詞雲分析的方法

5.對資料進行視覺化的方法

6.LDA隱含狄利克雷分佈模型的建模和使用方法

前言

最近做課設,是一個有關個人隱私安全的課題,在網上找了很多論文,最後上海交通大學的一篇碩士論文《面向社會工程學的SNS分析和挖掘》[1] 給了我很多靈感,因為是對個人隱私安全進行評估,所以我們基於微博社交網路獲取的資料進行分析。以下是該系列第一篇文章,記錄爬取微博使用者資訊的過程。

先決條件

我們這次的目標是爬取微博個人使用者的資料資訊和動態資訊並儲存在 mysql 資料庫中。

因為爬取微博主頁 weibo.com/ 或者 m.weibo.cn/ 較為困難,所以我們爬取 weibo.cn,這是一個落後的塞班年代的網頁,沒有混淆等等一系列新技術,使用者動態等從html裡面就可以獲取,爬取相對來說比較簡單。

首先要對網頁進行大致分析,獲取爬蟲的先決條件。

cookies

因為微博對訪客進行了限制,所以請求裡面沒有 cookies 的話動態無法抓取完全。

故我們也需要獲取 cookie:

  • 用Chrome開啟passport.weibo.cn/signin/logi…
  • 按F12鍵開啟Chrome開發者工具;
  • 點開“Network”,將“Preserve log”選中,輸入微博的使用者名稱、密碼,登入
  • 點選Chrome開發者工具“Name"列表中的"m.weibo.cn",點選"Headers",其中"Request Headers"下,"Cookie"後的值即為我們要找的cookie值,複製即可

UID

因為我們是抓取使用者資料,所以首先應該知道如何獲取唯一識別符號——uid,每個使用者的 uid 是不一樣的,有了 uid 我們才可以通過它來標識使用者。登入自己的賬戶以後,我們可以訪問使用者的資料頁(以張亞勤為例),可以看到頁面地址為 https://weibo.cn/1645171780/info,其中 "1645171780" 即為張亞勤的 uid,如圖所示:

n8Vrl.jpg

網頁分析

獲取了 uid 和 cookie ,我們來對網頁進行詳細分析。

使用者資料頁原始碼分析

因為資料頁資料量很少,分析和處理都比較容易,所以首先從這裡下手。

由上一張圖片可以看出,資料頁分成 **基本資訊 ,學習經歷,工作經歷,其他資訊 ** 四個模組,我們只需要前三個模組就可以了。分析原始碼的 html ,我們發現 class="tip"> 剛好可以標識四個資訊模組,而對於每個模組內部的資料條目,class="c" 可以進行一一標識,如圖所示:

n8xVJ.jpg

使用正規表示式進行匹配,關於正規表示式的使用方法,請看我的另一篇文章。程式碼如下:

tip = re.compile(r'class="tip">(.*?)></div>', re.S) #匹配四個模組所有內容
title = re.compile(r'(.*?)</div><div', re.S)  # 匹配基本資訊/學習經歷/工作經歷/其他資訊
node = re.compile(r'.*?class="c"(.*?)$', re.S) # 匹配一個模組中的所有內容
info = re.compile(r'>(.*?)<br/', re.S) # 匹配資料條
複製程式碼

使用者動態頁原始碼分析

對於一頁的動態來說很好分析,每一條動態內容前面都有 <span class="ctt">,並且一一對應。而動態釋出時間一一對應 <span class="ct"> ,如圖所示:

n8QzB.jpg

正規表示式程式碼如下:

dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S)  # 匹配動態
times = re.compile(r'.*?<span class="ct">(.*?)&nbsp', re.S)  # 匹配動態釋出時間
複製程式碼

可以從第一頁中獲取頁數:

page_number = re.compile(r'.*/(\d*?)頁</div>', re.S)  # 匹配動態頁數
複製程式碼

爬取資訊

有了前面的鋪墊,爬取使用者資料便比較容易實現了。

對於使用者資料,使用前面的正規表示式對爬去的頁面進行處理,有以下程式碼:

tip = re.compile(r'class="tip">(.*?)></div>', re.S) #匹配四個模組所有內容
title = re.compile(r'(.*?)</div><div', re.S)  # 匹配基本資訊/學習經歷/工作經歷/其他資訊
node = re.compile(r'.*?class="c"(.*?)$', re.S) # 匹配一個模組中的所有內容
info = re.compile(r'>(.*?)<br/', re.S) # 匹配資料條
Uname = ''
Certified = ''
Sex = ''
Relationship = ''
Area = ''
Birthday = ''
Education_info = ''
Work_info = ''
Description = ''
for one in tips:
    titleone = re.findall(title, one)  # 資訊標題

    node_tmp = re.findall(node, one)
    infos = re.findall(info, node_tmp[0])  # 資訊
    if (titleone[0] == '基本資訊'):
        for inf in infos:
            if (inf.startswith('暱稱')):
                _, Uname = inf.split(':', 1)
            elif (inf.startswith('認證資訊')):
                print(inf)
                _, Certified = inf.split(':', 1)
            elif (inf.startswith('性別')):
                _, Sex = inf.split(':', 1)
            elif (inf.startswith('感情狀況')):
                _, Relationship = inf.split(':', 1)
            elif (inf.startswith('地區')):
                _, Area = inf.split(':', 1)
            elif (inf.startswith('生日')):
                _, Birthday = inf.split(':', 1)
            elif (inf.startswith('簡介')):
                print(inf.split(':'))
                _, Description = inf.split(':', 1)
            else:
                pass
    elif (titleone[0] == '學習經歷'):
        for inf in infos:
            Education_info += inf.strip('·').replace("&nbsp", '') + " "
    elif (titleone[0] == '工作經歷'):
        for inf in infos:
            Work_info += inf.strip('·').replace("&nbsp", '') + " "
    else:
        pass
複製程式碼

而對於使用者動態資訊,處理的程式碼:

dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S)  # 匹配動態
times = re.compile(r'.*?<span class="ct">(.*?)&nbsp', re.S)  # 匹配動態釋出時間
page_number = re.compile(r'.*/(\d*?)頁</div>', re.S)  # 匹配動態頁數
dys = re.findall(dynamic, res.text)
ts = re.findall(times, res.text)
pages = re.findall(page_number, res.text)
pagenums = pages[0]

mainurl = url
label = 0  # 標籤用於計數,每5~20次延時10S
tag = random.randint(5, 20)
for pagenum in range(int(pagenums))[1:]:
    if (label == tag):
        time.sleep(10)
        label = 0
        tag = random.randint(5, 20)
    # 隨機選擇,防止被ban
    cookie = random.choice(cookies)
    cookie = getcookies(cookie)
    headers = {
        'User_Agent': random.choice(user_agents)
    }
    pagenum += 1
    label += 1
    url = mainurl + '?page=' + str(pagenum)#更改頁數
    page = gethtml(url, headers, cookie, conf, use_proxies)
    dys += re.findall(dynamic, page.text)
    ts += re.findall(times, page.text)
dys = dys[1:]
複製程式碼

至此爬蟲這部分程式碼基本上完成。

儲存資料到資料庫

如果沒有儲存在資料庫的需要,可以不用閱讀該部分。

本來之前是使用 pymysql + SQL語句實現資料庫操作,但是這樣太繁瑣了,並且這些訪問資料庫的程式碼如果分散到各個函式中,勢必無法維護,也不利於程式碼複用。所以在這裡我使用ORM框架(SQLAlchemy)來運算元據庫,該框架實現了對資料庫的對映操作,即封裝了資料庫操作,簡化程式碼邏輯。

首先建立三個表:

# 微博使用者資訊表
    wb_user = Table('wb_user', metadata,
                    Column('user_ID', Integer, primary_key=True, autoincrement=True),  # 主鍵,自動新增
                    Column("uid", String(20), unique=True, nullable=False),  # 微博使用者的uid
                    Column("Uname", String(50), nullable=False),  # 暱稱
                    Column("Certified", String(50), default='', server_default=''),  # 認證資訊
                    Column("Sex", String(200), default='', server_default=''),  # 性別nullable=False
                    Column("Relationship", String(20), default='', server_default=''),  # 感情狀況
                    Column("Area", String(500), default='', server_default=''),  # 地區
                    Column("Birthday", String(50), default='', server_default=''),  # 生日
                    Column("Education_info", String(300), default='', server_default=''),  # 學習經歷
                    Column("Work_info", String(300), default='', server_default=''),  # 工作經歷
                    Column("Description", String(2500), default='', server_default=''),  # 簡介
                    mysql_charset='utf8mb4'
                    )

    # 微博使用者動態表
    wb_data = Table('wb_data', metadata,
                    Column('data_ID', Integer, primary_key=True, autoincrement=True),  # 主鍵,自動新增
                    Column('uid', String(20), ForeignKey(wb_user.c.uid), nullable=False),  # 外來鍵
                    Column('weibo_cont', TEXT, default=''),  # 微博內容
                    Column('create_time', String(200), unique=True),  # 建立時間,unique用來執行upsert操作,判斷衝突
                    mysql_charset='utf8mb4'
                    )

    # 動態主題表
    wb_topic = Table('wb_topic', metadata,
                     Column('topic_ID', Integer, primary_key=True, autoincrement=True),  # 主鍵,自動新增
                     Column('uid', String(20), ForeignKey(wb_user.c.uid), nullable=False),  # 外來鍵
                     Column('topic', Integer, nullable=False),  # 主題-----預設5類
                     Column('topic_cont', String(20), nullable=False, unique=True),  # 主題內容
                     mysql_charset='utf8mb4'
                     )
複製程式碼

這裡有一個細節需要注意,那就是 mysql 的編碼使用了utf8m64的編碼方式,為什麼要使用這種方式呢?因為微博裡面的emoji 表情佔4個位元組,超過了utf-8 編碼範圍:UTF-8 是 3 個位元組,其中已經包括我們日常能見過的絕大多數字體,但 3 個位元組遠遠不夠容納所有的文字, 所以便有了utf8mb4 , utf8mb4 是 utf8 的超集,佔4個位元組, 向下相容utf8。使用 utf8mb4 要求:

MySQL資料庫版本>=5.5.3

MySQL-python 版本 >= 1.2.5

然後我們將爬蟲獲取的資訊存到資料庫中,首先是資料頁資料:

from sqlalchemy import MetaData, Table
from sqlalchemy.dialects.mysql import insert
ins = insert(table).values(uid=uid, Uname=Uname, Certified=Certified, Sex=Sex, Relationship=Relationship,Area=Area,Birthday=Birthday,Education_info=Education_info,Work_info=Work_info,Description=Description)
ins = ins.on_duplicate_key_update(
# 如果不存在則插入,存在則更新(upsert操作#http://docs.sqlalchemy.org/en/latest/dialects/mysql.html#mysql-insert-on-duplicate-key-#update)
    Uname=Uname, Certified=Certified, Sex=Sex, Relationship=Relationship, Area=Area,
    Birthday=Birthday, Education_info=Education_info, Work_info=Work_info, Description=Description
)
conn.execute(ins)
複製程式碼

接著是動態資料儲存在資料庫中:

re_nbsp = re.compile(r'&nbsp', re.S)  # 去除$nbsp
re_html = re.compile(r'</?\w+[^>]*>', re.S)  # 去除html標籤
re_200b = re.compile(r'\u200b', re.S)  # 去除分隔符
re_quot = re.compile(r'&quot', re.S)
for i in range(len(ts)):#len(ts)為動態數
	#去除噪聲
    dys[i] = re_nbsp.sub('', dys[i])
    dys[i] = re_html.sub('', dys[i])
    dys[i] = re_200b.sub('', dys[i])
    dys[i] = re_quot.sub('', dys[i])
    ins = insert(table).values(uid=uid, weibo_cont=pymysql.escape_string(dys[i]), create_time=ts[i])
    ins = ins.on_duplicate_key_update(weibo_cont=pymysql.escape_string(dys[i]))
    conn.execute(ins)
複製程式碼

尾聲

整個爬蟲的資料獲取部分已經基本上介紹完畢,完整程式碼見 github.com/starFalll/S… .

下一篇介紹一下對獲取的資料進行處理的過程。

參考:[1]陸飛.面向社會工程學的SNS分析和挖掘[D].上海:上海交通大學,2013.

相關文章