爬蟲實(shí)戰(zhàn)(一):爬取微博用戶信息

原文地址:https://blogof33.com/post/11/

前言

最近做課設(shè),是一個(gè)有關(guān)個(gè)人隱私安全的課題,在網(wǎng)上找了很多論文,最后上海交通大學(xué)的一篇碩士論文《面向社會工程學(xué)的SNS分析和挖掘》[1] 給了我很多靈感,因?yàn)槭菍€(gè)人隱私安全進(jìn)行評估,所以我們基于微博社交網(wǎng)絡(luò)獲取的數(shù)據(jù)進(jìn)行分析。以下是該系列第一篇文章,記錄爬取微博用戶信息的過程。

先決條件

我們這次的目標(biāo)是爬取微博個(gè)人用戶的資料信息和動態(tài)信息并保存在 mysql 數(shù)據(jù)庫中。

因?yàn)榕廊∥⒉┲黜?https://weibo.com/ 或者 https://m.weibo.cn/ 較為困難,所以我們爬取 https://weibo.cn,這是一個(gè)落后的塞班年代的網(wǎng)頁,沒有混淆等等一系列新技術(shù),用戶動態(tài)等從html里面就可以獲取,爬取相對來說比較簡單。

首先要對網(wǎng)頁進(jìn)行大致分析,獲取爬蟲的先決條件。

cookies

因?yàn)槲⒉υL客進(jìn)行了限制,所以請求里面沒有 cookies 的話動態(tài)無法抓取完全。

故我們也需要獲取 cookie:

  • 用Chrome打開https://passport.weibo.cn/signin/login
  • 按F12鍵打開Chrome開發(fā)者工具;
  • 點(diǎn)開“Network”,將“Preserve log”選中,輸入微博的用戶名、密碼,登錄
  • 點(diǎn)擊Chrome開發(fā)者工具“Name"列表中的"m.weibo.cn",點(diǎn)擊"Headers",其中"Request Headers"下,"Cookie"后的值即為我們要找的cookie值,復(fù)制即可

UID

因?yàn)槲覀兪亲ト∮脩魯?shù)據(jù),所以首先應(yīng)該知道如何獲取唯一標(biāo)識符——uid,每個(gè)用戶的 uid 是不一樣的,有了 uid 我們才可以通過它來標(biāo)識用戶。登錄自己的賬戶以后,我們可以訪問用戶的資料頁(以張亞勤為例),可以看到頁面地址為 https://weibo.cn/1645171780/info,其中 "1645171780" 即為張亞勤的 uid,如圖所示:

用戶資料頁

網(wǎng)頁分析

獲取了 uid 和 cookie ,我們來對網(wǎng)頁進(jìn)行詳細(xì)分析。

用戶資料頁源碼分析

因?yàn)橘Y料頁數(shù)據(jù)量很少,分析和處理都比較容易,所以首先從這里下手。

由上一張圖片可以看出,資料頁分成 **基本信息 ,學(xué)習(xí)經(jīng)歷,工作經(jīng)歷,其他信息 ** 四個(gè)模塊,我們只需要前三個(gè)模塊就可以了。分析源碼的 html ,我們發(fā)現(xiàn) class="tip"> 剛好可以標(biāo)識四個(gè)信息模塊,而對于每個(gè)模塊內(nèi)部的資料條目,class="c" 可以進(jìn)行一一標(biāo)識,如圖所示:

資料頁源碼

使用正則表達(dá)式進(jìn)行匹配,關(guān)于正則表達(dá)式的使用方法,請看我的另一篇文章。代碼如下:

tip = re.compile(r'class="tip">(.*?)></div>', re.S) #匹配四個(gè)模塊所有內(nèi)容
title = re.compile(r'(.*?)</div><div', re.S)  # 匹配基本信息/學(xué)習(xí)經(jīng)歷/工作經(jīng)歷/其他信息
node = re.compile(r'.*?class="c"(.*?)$', re.S) # 匹配一個(gè)模塊中的所有內(nèi)容
info = re.compile(r'>(.*?)<br/', re.S) # 匹配資料條

用戶動態(tài)頁源碼分析

對于一頁的動態(tài)來說很好分析,每一條動態(tài)內(nèi)容前面都有 <span class="ctt">,并且一一對應(yīng)。而動態(tài)發(fā)布時(shí)間一一對應(yīng) <span class="ct"> ,如圖所示:

動態(tài)頁源碼

正則表達(dá)式代碼如下:

dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S)  # 匹配動態(tài)
times = re.compile(r'.*?<span class="ct">(.*?)&nbsp', re.S)  # 匹配動態(tài)發(fā)布時(shí)間

可以從第一頁中獲取頁數(shù):

page_number = re.compile(r'.*/(\d*?)頁</div>', re.S)  # 匹配動態(tài)頁數(shù)

爬取信息

有了前面的鋪墊,爬取用戶資料便比較容易實(shí)現(xiàn)了。

對于用戶資料,使用前面的正則表達(dá)式對爬去的頁面進(jìn)行處理,有以下代碼:

tip = re.compile(r'class="tip">(.*?)></div>', re.S) #匹配四個(gè)模塊所有內(nèi)容
title = re.compile(r'(.*?)</div><div', re.S)  # 匹配基本信息/學(xué)習(xí)經(jīng)歷/工作經(jīng)歷/其他信息
node = re.compile(r'.*?class="c"(.*?)$', re.S) # 匹配一個(gè)模塊中的所有內(nèi)容
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)  # 信息標(biāo)題

    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('認(rèn)證信息')):
                print(inf)
                _, Certified = inf.split(':', 1)
            elif (inf.startswith('性別')):
                _, Sex = inf.split(':', 1)
            elif (inf.startswith('感情狀況')):
                _, Relationship = inf.split(':', 1)
            elif (inf.startswith('地區(qū)')):
                _, 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] == '學(xué)習(xí)經(jīng)歷'):
        for inf in infos:
            Education_info += inf.strip('·').replace("&nbsp", '') + " "
    elif (titleone[0] == '工作經(jīng)歷'):
        for inf in infos:
            Work_info += inf.strip('·').replace("&nbsp", '') + " "
    else:
        pass

而對于用戶動態(tài)信息,處理的代碼:

dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S)  # 匹配動態(tài)
times = re.compile(r'.*?<span class="ct">(.*?)&nbsp', re.S)  # 匹配動態(tài)發(fā)布時(shí)間
page_number = re.compile(r'.*/(\d*?)頁</div>', re.S)  # 匹配動態(tài)頁數(shù)
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  # 標(biāo)簽用于計(jì)數(shù),每5~20次延時(shí)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)
    # 隨機(jī)選擇,防止被ban
    cookie = random.choice(cookies)
    cookie = getcookies(cookie)
    headers = {
        'User_Agent': random.choice(user_agents)
    }
    pagenum += 1
    label += 1
    url = mainurl + '?page=' + str(pagenum)#更改頁數(shù)
    page = gethtml(url, headers, cookie, conf, use_proxies)
    dys += re.findall(dynamic, page.text)
    ts += re.findall(times, page.text)
dys = dys[1:]

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

保存數(shù)據(jù)到數(shù)據(jù)庫

如果沒有保存在數(shù)據(jù)庫的需要,可以不用閱讀該部分。

本來之前是使用 pymysql + SQL語句實(shí)現(xiàn)數(shù)據(jù)庫操作,但是這樣太繁瑣了,并且這些訪問數(shù)據(jù)庫的代碼如果分散到各個(gè)函數(shù)中,勢必?zé)o法維護(hù),也不利于代碼復(fù)用。所以在這里我使用ORM框架(SQLAlchemy)來操作數(shù)據(jù)庫,該框架實(shí)現(xiàn)了對數(shù)據(jù)庫的映射操作,即封裝了數(shù)據(jù)庫操作,簡化代碼邏輯。

首先創(chuàng)建三個(gè)表:

# 微博用戶信息表
    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=''),  # 認(rèn)證信息
                    Column("Sex", String(200), default='', server_default=''),  # 性別nullable=False
                    Column("Relationship", String(20), default='', server_default=''),  # 感情狀況
                    Column("Area", String(500), default='', server_default=''),  # 地區(qū)
                    Column("Birthday", String(50), default='', server_default=''),  # 生日
                    Column("Education_info", String(300), default='', server_default=''),  # 學(xué)習(xí)經(jīng)歷
                    Column("Work_info", String(300), default='', server_default=''),  # 工作經(jīng)歷
                    Column("Description", String(2500), default='', server_default=''),  # 簡介
                    mysql_charset='utf8mb4'
                    )

    # 微博用戶動態(tài)表
    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=''),  # 微博內(nèi)容
                    Column('create_time', String(200), unique=True),  # 創(chuàng)建時(shí)間,unique用來執(zhí)行upsert操作,判斷沖突
                    mysql_charset='utf8mb4'
                    )

    # 動態(tài)主題表
    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),  # 主題-----默認(rèn)5類
                     Column('topic_cont', String(20), nullable=False, unique=True),  # 主題內(nèi)容
                     mysql_charset='utf8mb4'
                     )

這里有一個(gè)細(xì)節(jié)需要注意,那就是 mysql 的編碼使用了utf8m64的編碼方式,為什么要使用這種方式呢?因?yàn)槲⒉├锩娴膃moji 表情占4個(gè)字節(jié),超過了utf-8 編碼范圍:UTF-8 是 3 個(gè)字節(jié),其中已經(jīng)包括我們?nèi)粘D芤娺^的絕大多數(shù)字體,但 3 個(gè)字節(jié)遠(yuǎn)遠(yuǎn)不夠容納所有的文字, 所以便有了utf8mb4 , utf8mb4 是 utf8 的超集,占4個(gè)字節(jié), 向下兼容utf8。使用 utf8mb4 要求:

MySQL數(shù)據(jù)庫版本>=5.5.3

MySQL-python 版本 >= 1.2.5

然后我們將爬蟲獲取的信息存到數(shù)據(jù)庫中,首先是資料頁數(shù)據(jù):

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)

接著是動態(tài)數(shù)據(jù)保存在數(shù)據(jù)庫中:

re_nbsp = re.compile(r'&nbsp', re.S)  # 去除$nbsp
re_html = re.compile(r'</?\w+[^>]*>', re.S)  # 去除html標(biāo)簽
re_200b = re.compile(r'\u200b', re.S)  # 去除分隔符
re_quot = re.compile(r'&quot', re.S)
for i in range(len(ts)):#len(ts)為動態(tài)數(shù)
    #去除噪聲
    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)

尾聲

整個(gè)爬蟲的數(shù)據(jù)獲取部分已經(jīng)基本上介紹完畢,完整代碼見 https://github.com/starFalll/Spider .

下一篇介紹一下對獲取的數(shù)據(jù)進(jìn)行處理的過程。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容