前段时间在整理爬虫知识体系的时候,本着实践出真理的出发点,特意小试牛刀,写了几个不同类型的爬虫。然而在写微博评论的爬虫的时候,意外的发现,微博评论的API竟然变了!据本姑娘记忆中搞一个微博评论的爬虫简直就是飒飒随啦:(1)在谷歌浏览器中以移动端的方式请求访问微博,(2)分析请求列表,找到结构化json的源url,(3)分析url规律,上手撸代码,Bingo!
特意去Google了一下,确认本姑娘的思路没问题,并且获取评论数据的旧API接口url是这个样子滴:https://m.weibo.cn/api/comments/show?id={id}&page={page}
,此处的id
表示要爬的微博的id,page
表示第几页的评论数据,同时惊奇的发现,目前这个接口还是可用的(可能还没停止使用,只不过在最新的评论的url中已经找不到了)如:
然而作为一个有理想、有追求,求上进的优秀青年肯定不会止步与此,将就着用老接口来爬数据的!
通过以上的步骤分析,我们在谷歌浏览器中打开微博,请求移动端页面,找json数据源url,如下图所示,看到response中得到的json字符串,看到那令人兴奋的红色的ok和data,没错,就是它了。
然后我们提取这个url,给一个特写!
https://m.weibo.cn/comments/hotflow?id=4282494510984677&mid=4282494510984677&max_id=225416516976479&max_id_type=0
可以看到在最新的接口url中,取消了之前的page
字段,取而代之的是三个新字段,mid
、max_id
、max_id_type
。内心OS:“呵,青铜操作而已啦!”。当然了,结果肯定是要被打脸的,打开此url,得到的结果是这样的:
不得不说有点意思,你已经成功吸引了本姑娘注意力了。
然后呢,我们进行了一波分析,主要包括了以下几个方面:
(1)url地址肯定是没错的,为什么再次打开的时候,得到的结果是{“ok”:0}
,可以看到正确的返回结果中,ok对应的值应该是1的。两次访问同一url得到结果不一致是什么操作?
(2)新接口相对老接口,取消了page
字段,增加了mid
、max_id
、max_id_type
,mid
同微博id一致,因此此字段暂不考虑。那么服务器端是如何通过剩下这两个字段来区分第几页数据?
(3)返回正确结果的请求和返回错误结果的请求两次发起的请求携带的信息会有什么不一致吗?
针对以上几点,经过反复确认尝试,通过fiddler进行多次的抓包分析,得到结果:
(1)确认排除get/post请求方式引起的错误。
(2)对max_id
字段数据分析,确认排除通过时间戳来区分不同页数。
(3)确认排除由于两次发起的cookie等信息的不同而被反爬虫。
此时此刻本姑娘已经不开心了,内心已有被调戏的不爽。所以高低一定要拿下,胜利的号角必须要吹响!
先喝杯卡布奇诺冷静一下,再分析接下来应该从哪里入手。从接口url上来看,新老接口的最大不同就在于新接口多了mid
,max_id
,max_id_type
三个字段。而且经过了缜密侦查发现response中竟然也有max_id
、max_id_type
的身影出现。哈!到了这里本姑娘敏锐的感觉好像已经抓到什么了,仿佛已经看到了微博后台小哥哥漏出的半截狐狸尾巴了。
然后本姑娘就猜测,评论数据的第一页的url一定是有所不同滴,让我们来验证一下下,给丫的一个特写!
https://m.weibo.cn/comments/hotflow?id=4282494510984677&mid=4282494510984677&max_id_type=0
看到没有,看到没有!此url链接中没有max_id
,哈哈哈哈哈哈哈哈哈,扬天长啸出门去,我辈岂是蓬篙人啊!到这里要先卖个关子,先让我们打开这个链接看看会是什么情况呢?
Bingo!果然这是一个与众不同的url,如此的光彩夺目的一个url!第一页的url在地址栏中打开是可以返回数据滴!
好啦,综上三点(1)在response中的json数据中有max_id字段,(2)评论第一页的url中没有max_id字段,(3)评论第一页的url是可以反复多次打开的。本姑娘大胆猜测,把第一页的url的response中的max_id的值拼接到评论第一页的url上得到的结果一定是评论第二页的数据!接下来就是见证奇迹的时刻了:
Bingo!完成!
也就是说除了评论数据的第一页的url上不带有max_id
字段以外,其他的请求发起时候时,均需携带有上次请求response中返回的max_id
数据。例如第二次请求需要携带第一次请求返回的max_id
,第三次请求需要携带第二次请求返回的max_id
……,依次下去。同时这个顺序是不可逆的,比如当前已经返回了第六页的数据了,此时我们再想访问第三页的数据是不允许的(因为正常操作也是不会出现这种情况滴),而且对同一页的数据进行快速连续访问多次也是不合理的,后台小哥一样会丢给你一个{‘ok’:0}
至此分析部分已经完成,接下来就是愉快滴撸代码啦。首先要爬评论是需要先登陆的,通过登陆获取cookie,之后的请求均携带此cookie。有了cookie了,接下来就是按照上面分析的规则进行评论的爬爬爬爬爬爬爬爬爬,先简单的用Request来走一波,改天上个scrapy版本/scrapy-redis版本。
# -*- coding: utf-8 -*-
import time
import base64
import rsa
import binascii
import requests
import re
from PIL import Image
import random
from urllib.parse import quote_plus
import http.cookiejar as cookielib
import json
agent = 'mozilla/5.0 (windowS NT 10.0; win64; x64) appLewEbkit/537.36 (KHTML, likE gecko) chrome/71.0.3578.98 safari/537.36'
headers = {'User-Agent': agent}
class WeiboLogin(object):
"""
通过登录 weibo.com 然后跳转到 m.weibo.cn
"""
#初始化数据
def __init__(self, user, password, cookie_path):
super(WeiboLogin, self).__init__()
self.user = user
self.password = password
self.session = requests.Session()
self.cookie_path = cookie_path
# LWPCookieJar是python中管理cookie的工具,可以将cookie保存到文件,或者在文件中读取cookie数据到程序
self.session.cookies = cookielib.LWPCookieJar(filename=self.cookie_path)
self.index_url = "http://weibo.com/login.php"
self.session.get(self.index_url, headers=headers, timeout=2)
self.postdata = dict()
def get_su(self):
"""
对 email 地址和手机号码 先 javascript 中 encodeURIComponent
对应 Python 3 中的是 urllib.parse.quote_plus
然后在 base64 加密后decode
"""
username_quote = quote_plus(self.user)
username_base64 = base64.b64encode(username_quote.encode("utf-8"))
return username_base64.decode("utf-8")
# 预登陆获得 servertime, nonce, pubkey, rsakv
def get_server_data(self, su):
"""与原来的相比,微博的登录从 v1.4.18 升级到了 v1.4.19
这里使用了 URL 拼接的方式,也可以用 Params 参数传递的方式
"""
pre_url = "http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su="
pre_url = pre_url + su + "&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.19)&_="
pre_url = pre_url + str(int(time.time() * 1000))
pre_data_res = self.session.get(pre_url, headers=headers)
# print("*"*50)
# print(pre_data_res.text)
# print("*" * 50)
sever_data = eval(pre_data_res.content.decode("utf-8").replace("sinaSSOController.preloginCallBack", ''))
return sever_data
def get_password(self, servertime, nonce, pubkey):
"""对密码进行 RSA 的加密"""
rsaPublickey = int(pubkey, 16)
key = rsa.PublicKey(rsaPublickey, 65537) # 创建公钥
message = str(servertime) + '\t' + str(nonce) + '\n' + str(self.password) # 拼接明文js加密文件中得到
message = message.encode("utf-8")
passwd = rsa.encrypt(message, key) # 加密
passwd = binascii.b2a_hex(passwd) # 将加密信息转换为16进制。
return passwd
def get_cha(self, pcid):
"""获取验证码,并且用PIL打开,
1. 如果本机安装了图片查看软件,也可以用 os.subprocess 的打开验证码
2. 可以改写此函数接入打码平台。
"""
cha_url = "https://login.sina.com.cn/cgi/pin.php?r="
cha_url = cha_url + str(int(random.random() * 100000000)) + "&s=0&p="
cha_url = cha_url + pcid
cha_page = self.session.get(cha_url, headers=headers)
with open("cha.jpg", 'wb') as f:
f.write(cha_page.content)
f.close()
try:
im = Image.open("cha.jpg")
im.show()
im.close()
except Exception as e:
print(u"请到当前目录下,找到验证码后输入")
def pre_login(self):
# su 是加密后的用户名
su = self.get_su()
sever_data = self.get_server_data(su)
servertime = sever_data["servertime"]
nonce = sever_data['nonce']
rsakv = sever_data["rsakv"]
pubkey = sever_data["pubkey"]
showpin = sever_data["showpin"] # 这个参数的意义待探索
password_secret = self.get_password(servertime, nonce, pubkey)
self.postdata = {
'entry': 'weibo',
'gateway': '1',
'from': '',
'savestate': '7',
'useticket': '1',
'pagerefer': "https://passport.weibo.com",
'vsnf': '1',
'su': su,
'service': 'miniblog',
'servertime': servertime,
'nonce': nonce,
'pwencode': 'rsa2',
'rsakv': rsakv,
'sp': password_secret,
'sr': '1366*768',
'encoding': 'UTF-8',
'prelt': '115',
"cdult": "38",
'url': 'http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack',
'returntype': 'TEXT' # 这里是 TEXT 和 META 选择,具体含义待探索
}
return sever_data
def login(self):
# 先不输入验证码登录测试
try:
sever_data = self.pre_login()
login_url = 'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.19)&_'
login_url = login_url + str(time.time() * 1000)
login_page = self.session.post(login_url, data=self.postdata, headers=headers)
ticket_js = login_page.json()
ticket = ticket_js["ticket"]
except Exception as e:
sever_data = self.pre_login()
login_url = 'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.19)&_'
login_url = login_url + str(time.time() * 1000)
pcid = sever_data["pcid"]
self.get_cha(pcid)
self.postdata['door'] = input(u"请输入验证码")
login_page = self.session.post(login_url, data=self.postdata, headers=headers)
ticket_js = login_page.json()
ticket = ticket_js["ticket"]
# 以下内容是 处理登录跳转链接
save_pa = r'==-(\d+)-'
ssosavestate = int(re.findall(save_pa, ticket)[0]) + 3600 * 7
jump_ticket_params = {
"callback": "sinaSSOController.callbackLoginStatus",
"ticket": ticket,
"ssosavestate": str(ssosavestate),
"client": "ssologin.js(v1.4.19)",
"_": str(time.time() * 1000),
}
jump_url = "https://passport.weibo.com/wbsso/login"
jump_headers = {
"Host": "passport.weibo.com",
"Referer": "https://weibo.com/",
"User-Agent": headers["User-Agent"]
}
jump_login = self.session.get(jump_url, params=jump_ticket_params, headers=jump_headers)
uuid = jump_login.text
uuid_pa = r'"uniqueid":"(.*?)"'
uuid_res = re.findall(uuid_pa, uuid, re.S)[0]
web_weibo_url = "http://weibo.com/%s/profile?topnav=1&wvr=6&is_all=1" % uuid_res
weibo_page = self.session.get(web_weibo_url, headers=headers)
# print(weibo_page.content.decode("utf-8")
Mheaders = {
"Host": "login.sina.com.cn",
"User-Agent": agent
}
# m.weibo.cn 登录的 url 拼接
_rand = str(time.time())
mParams = {
"url": "https://m.weibo.cn/",
"_rand": _rand,
"gateway": "1",
"service": "sinawap",
"entry": "sinawap",
"useticket": "1",
"returntype": "META",
"sudaref": "",
"_client_version": "0.6.26",
}
murl = "https://login.sina.com.cn/sso/login.php"
mhtml = self.session.get(murl, params=mParams, headers=Mheaders)
mhtml.encoding = mhtml.apparent_encoding
mpa = r'replace\((.*?)\);'
mres = re.findall(mpa, mhtml.text)
# 关键的跳转步骤,这里不出问题,基本就成功了。
Mheaders["Host"] = "passport.weibo.cn"
self.session.get(eval(mres[0]), headers=Mheaders)
mlogin = self.session.get(eval(mres[0]), headers=Mheaders)
# print(mlogin.status_code)
# 进过几次 页面跳转后,m.weibo.cn 登录成功,下次测试是否登录成功
Mheaders["Host"] = "m.weibo.cn"
Set_url = "https://m.weibo.cn"
pro = self.session.get(Set_url, headers=Mheaders)
pa_login = r'isLogin":true,'
login_res = re.findall(pa_login, pro.text)
# print(login_res)
# 可以通过 session.cookies 对 cookies 进行下一步相关操作
self.session.cookies.save()
# print("*"*50)
# print(self.cookie_path)
def weibo_comment():
max_id = ""
headers = {"user-agent": "mozilla/5.0 (windowS NT 10.0; win64; x64) appLewEbkit/537.36 (KHTML, likE gecko) chrome/71.0.3578.98 safari/537.36"}
#加载cookie
cookies = cookielib.LWPCookieJar("Cookie.txt")
cookies.load(ignore_discard=True, ignore_expires=True)
# 将cookie转换成字典
cookie_dict = requests.utils.dict_from_cookiejar(cookies)
while True:
if max_id == "":
url = "https://m.weibo.cn/comments/hotflow?id=4344811987373681&mid=4344811987373681&max_id_type=0"
else:
url = "https://m.weibo.cn/comments/hotflow?id=4344811987373681&mid=4344811987373681&max_id="+str(max_id)+"&max_id_type=0"
# print(url)
response = requests.get(url, headers=headers, cookies=cookie_dict)
comment = response.json()
if comment['ok'] == 0:
break
max_id = comment["data"]["max_id"]
# print([data_1["text"] for data_1 in comment["data"]["data"]])
line = []
for comment_data in comment["data"]["data"]:
data = comment_data["text"]
p = re.compile(r'(.*)*(.* a>)?')
data = p.sub(r'', data)
if len(data) != 0:
line.append(data)
time.sleep(1)
print(line)
if __name__ == '__main__':
username = "********" # 用户名
password = "********" # 密码
cookie_path = "Cookie.txt" # 保存cookie 的文件名称
weibo = WeiboLogin(username, password, cookie_path)
weibo.login()
weibo_comment()
大功告成! 晚餐要加鸡腿!
**未经本姑娘允许,禁止转载哈!