有一天我突然发现自己空间的说说竟然已经达到1833条,于是萌生了爬一下看看的想法(其实就是想学下python爬虫)。我找了一些博客,方法不少,但是有些并不适用。所以我把真正能用的方法记录下来,并且爬取了我自己的全部说说,亲测可用。下面我介绍下爬虫的写法。
selenium,requests,json,sqlite3,re,time,random
其中,selenium是用于模拟QQ空间登录的库,即一些动态页面的操作;requests爬虫常用库,不赘述;sqlite3用来存爬取的数据,python3之后的版本自带;re正则表达式,用来提取一些匹配字段。
我们先打开自己的QQ空间,点击说说,会跳转到https://user.qzone.qq.com/your qq number/infocenter这样一个页面,查看网页源码,搜索说说的具体内容,我们是无法找到的。这说明说说的内容、点赞数、评论等都不是静态页面的一部分,而是通过Ajax等手段访问后台加载得到的,因此,我们需要模拟浏览页面的过程,即需要用selenium。
仔细查看浏览器控制台中,筛选出XHR部分的响应正文。可以得到两个重要的url,如下:
第1条链接的响应正文包含了丰富的信息,格式是json。分析json格式,可以发现具体的内容都在msglist数组中,正常情况下一般是返回20条说说,所以数组长度是20。其中,有非常多可用的信息,如conlist属性下包括说说的具体内容(con),说说发表的时间戳(created_time),以及位置信息(lbs),都可以用来做一些相关的分析和可视化。这里提取content,cmtnum,tid和created_time,分别存储了说说具体内容,评论数,tid后面会用到,是一条说说的标识,以及说说发表时的时间戳。先将这四部分存入sqlite数据库中。
第2条链接是获取一些数值指标的接口,例如点赞数、评论数等等,这里为了获取点赞数,用第1个接口就无法获取,因此需要从数据库获取tid,根据它作为参数之一从第2条说说获取点赞数,再更新数据库,完成爬取任务。
以上就是爬虫的整体思路。
先创建一个sqlite数据库,用于存储爬去的数据,会在.py文件目录中生成一个.db文件数据库,代码如下:
#coding = 'utf-8'
import sqlite3
def create(dbname,sql):
conn = sqlite3.connect(dbname)
c = conn.cursor()
c.execute(sql)
conn.commit()
conn.close()
def main():
dbname = 'mineqqzone.db'
tablename = 'qqzoneinfo'
createsql = '''
create table qqzoneinfo
(id integer primary key autoincrement,#id
comment text,#说说内容
cmtnum int,#评论数
likenum int,#点赞数
tid text,#参数
createtime long)#时间戳
'''
create(dbname,createsql)
main()
首先要模拟登陆,具体代码在start_login()函数中,需要注意的是,获取g_tk这个参数的方法是对cookie中的p_skey进行加密得到,具体代码在get_g_tk()函数中,这段代码网上找找都找得到。
用selenium模拟浏览器需要下载例如phantomJS、Chromedriver这类的驱动,网上大部分是Chromedriver,但是我试了下配置似乎十分麻烦,所以选择phantomJS(无界面的浏览器),下载的链接在这http://phantomjs.org/download.html,可以下载对应版本。使用时只需在webdriver.PhantomJS()中把phantomjs.exe的路径赋给executable_path即可。虽然selenium现在的版本好像已经把phantomjs剔除了,但是似乎不影响使用。
qzonetoken参数可以从登陆后的html页面中提取,用正则表达式。
#coding = 'utf-8'
from selenium import webdriver
import requests
import json
from lxml import etree
from bs4 import BeautifulSoup
import sqlite3
import re
import time
import random
def get_g_tk(cookie):#获取g_tk参数,由cookie中的p_skey加密得到
hashes = 5381
for letter in cookie['p_skey']:
hashes += (hashes << 5) + ord(letter) # ord()是用来返回字符的ascii码
return hashes & 0x7fffffff
def start_login():#模拟登录QQ空间
driver = webdriver.PhantomJS(executable_path = "E:\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe")
url = "https://qzone.qq.com/"
driver.get(url)
driver.switch_to.frame('login_frame')
driver.find_element_by_id('switcher_plogin').click()
driver.find_element_by_id('u').clear()
driver.find_element_by_id('u').send_keys('你的QQ号')
driver.find_element_by_id('p').clear()
driver.find_element_by_id('p').send_keys('你的QQ密码')
driver.find_element_by_id('login_button').click()
time.sleep(3)#等待加载完毕
html = driver.page_source
g_qzonetoken = re.search('window\.g_qzonetoken = \(function\(\)\{ try\{return (.*?);\} catch\(e\)',html)
qzonetoken = str(g_qzonetoken[0]).split('\"')[1]#获取参数之一
cookie_list = driver.get_cookies()#获取cookie
cookie_dict = {}
for cookie in cookie_list:
cookie_dict[cookie['name']]=cookie['value']
g_tk = get_g_tk(cookie_dict)
return cookie_dict,g_tk,qzonetoken
我们开始爬取评论、说说、评论数、时间戳,代码如下:
def sqlopr(conn,cur,item):#存入数据库
cur.execute("insert into qqzoneinfo (comment,cmtnum,tid,createtime) values (?,?,?,?)",(item['content'],int(item['cmtnum']),item['tid'],item['created_time']))
conn.commit()
def spidercmt():#爬取评论
cookie,g_tk,qzonetoken = start_login()
headers={
'User-Agent':'打开浏览器控制台查看,复制过来即可'
}
conn = sqlite3.connect('mineqqzone.db')
cur = conn.cursor()
sessions = requests.session()
count = int(你的说说总数/20) + 1
for i in range(count):
pos = 当前爬取的说说数量减1 + i*20#因为有可能中途会爬取出错,你可以改变pos继续爬,哈哈哈
param = {
'uin': '你的QQ号',
'ftype': '0',
'sort': '0',
'pos': pos,
'num': '20',#爬取的说说条数,网页上默认是20条
'replynum': '100',
'g_tk': [g_tk, g_tk],#用在这
'callback': '_preloadCallback',
'code_version': '1',
'format': 'jsonp',
'need_private_comment': '1',
'qzonetoken': qzonetoken#用在这
}
respond = sessions.get('https://h5.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6',params=param,headers=headers,cookies=cookie)
r = re.sub("_preloadCallback","",respond.text)
test = r[1:-2]
Data = json.loads(test)
if not re.search('lbs',test):
print('说说下载完成')
else:
for j in range(len(Data['msglist'])):
sqlopr(conn,cur,Data['msglist'][j])
time.sleep(2)#可以改时间
print("suc"+str(i))
以下是爬取点赞数的代码:
def spiderlikenum():
cookie,g_tk,qzonetoken = start_login()
conn = sqlite3.connect('mineqqzone.db')
cur = conn.cursor()
sessions = requests.session()
count = len(cur.execute('select * from qqzoneinfo where likenum is Null').fetchall())
url = 'http://user.qzone.qq.com/你的QQ号/mood/'
while count != 0:
ress = cur.execute('select * from qqzoneinfo where likenum is Null').fetchall()
count = len(ress)-1
idc = ress[0][0]
tid = ress[0][4]
_stp = int(round(time.time() * 1000)) #时间戳
param = {
'_stp': str(_stp),#时间戳是一个参数
'unikey': url + tid + '.1<.>' + url + '.1',
'g_tk': [g_tk, g_tk],
'face': '0',
'fupdate': '1',
'qzonetoken': qzonetoken
}
respond = sessions.get('https://user.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/user/qz_opcnt2',params=param,headers=headers,cookies=cookie)
r = re.sub("_Callback","",respond.text)
test = r[1:-2]
if test == None or test == "" or len(test) == 0:
print("lose data")
continue
else:
try:
Data = json.loads(test)
likenum = Data['data'][0]['current']['likedata']['cnt']
cur.execute("update qqzoneinfo set likenum = "+str(likenum)+" where id = "+str(idc))
conn.commit()
print('like:'+str(likenum))
print(idc)
ran = random.random()
time.sleep(1+ran)#增加随机性,1-2秒访问一次
except:
print("error")
第2条链接的unikey参数一般是按照多条(例如20条)说说的tid进行组装,再访问链接,但是为了方便访问和解析,这里我每次只访问一条说说,这样unikey的组装相对简单明了,返回的响应正问也结构清楚,这样做的唯一缺点就是效率降低很多,只能一条条爬,按照每条1-2秒计算,如果中途不出问题,1800条至少要30min~1h,因此这部分可以根据情况改进,比如多线程、并行等都可以。
爬取点赞数代码的原理是不断访问数据库剩余未爬取点赞数的说说,while循环获取点赞数,直到全部获取为止。
以上就是全部的爬取代码。
爬取了所有的说说数据,就可以做任何想做的可视化咯。可视化可以用echarts的python版本,非常方便,链接在http://pyecharts.org
以下有一些分析的效果图,用了Scatter3D,Scatter,Bar,Pie,HeatMap这些基本表。
分析之后可以发现,大学之前的点赞和评论都极少,说明人际交友等都很少,大学之后(2014-2018)这段期间,虽然说说发表的数量每年都在减少,话越来越少,但是获得高赞和高评论的次数似乎不减反增,说明交际的质量提高了。查看一下点赞数和评论数最高的十条说说,不尽相同,也非常有意思,如下图。
女票霸榜了!有种重翻日记本的感觉~
最后也可以做成日历热力图,可以把每年的说说都展示出来,频数一目了然。这里就展示出两年的图。
以上就是最近这个idea的实现,完整的数据爬取和数据可视化的流程,feeling good。这篇文章核心是分享下爬虫的写法,因此只展示了爬虫部分的代码。可视化代码相对简单,有空再把可视化的代码展示一下~