原文地址: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"> ,如圖所示:

正則表達(dá)式代碼如下:
dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S) # 匹配動態(tài)
times = re.compile(r'.*?<span class="ct">(.*?) ', 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(" ", '') + " "
elif (titleone[0] == '工作經(jīng)歷'):
for inf in infos:
Work_info += inf.strip('·').replace(" ", '') + " "
else:
pass
而對于用戶動態(tài)信息,處理的代碼:
dynamic = re.compile(r'.*?><span class="ctt">(.*?)<a href', re.S) # 匹配動態(tài)
times = re.compile(r'.*?<span class="ct">(.*?) ', 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' ', 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'"', 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.