首先观察网站,明确爬取目标
经过观察,发现这个网站的数据都是异步加载的,而我此次爬取的目标,是一点资讯-段子模块下的内容
分析目标站点
既然是异步加载的,就可以在控制台的 Network标签下的xhr里面可以看到,异步请求的地址。如下
还有可以看到请求的参数
尝试请求数据
有了请求地址,有了请求所需的参数,肯定会先试验一波。
经过试验,发现这个地址是正确的,会返回json数据,而这个json数据正式我所需要的内容,可是只有很少一部分。
既然确定了请求地址,那么接下来就开始观察参数
此时呢,就去把那个页面往下拉,就像正常浏览网页一样,同时观察这个网站发出的请求(我这里并没有用fiddle之类的抓包工具,使用的chrom浏览器的开发者工具,很方便)。
点开观察里面参数的变化,一个一个来分析。
- channel_id 翻译过来就是频道id,经过请求了很多次,发现这个id是不会变的
- cstart: 50 这就像是页码一样的东西,这里算是开始
- cend: 60 这里是结束,中间间隔为+10
- infinite: true 这是不会变的
- refresh: 1 这个也不变
- _from_: pc 这应该是识别你是从pc端还是移动端,这里一直都是pc端,因为是用电脑访问的
- multi: 5 这个不知道什么意思,但是这个是固定值,也不变
- _spt: yz~eaod;82999:=9<>?:<: 这个参数暂时看不懂,看样子应该是用js处理过的
- appid: web_yidian 不变的
- _: 1535115263998 这个嘛,嘿嘿,时间按戳
好了,经过分析,返现只有_spt这个字段不能确定,但是猜测他是从js里处理出来的。
去搞定_spt这个参数
既然猜测是从js里处理出来的,就直接f12,shift+ctrl+f,全局搜索spt。
还真的发现这个东西在某个js文件里出现了。果断点进去,暗中观察。
就是这里,但是此时我看见js代码以后,老毛病犯了,开始头晕,恶心,四肢无力等症状。
可是没办法,想拿到我想要的东西,就得搞定spt参数,而这些前提就是把这里得js搞定。
然后就开始吧,打断点,调试,仔细观察,肯定先从参数入手啊。得知道这些参数是什么,怎么来的。
(图是后来又去打了断点补的,所以可能跟我当时观察的不一样,不过也差不多,关键的地方都在了)
- n 是固定的,链接的一部分
- e 是之前参数里的channel_id
- i 就是 cstart
然后一直f8,运行到最后,会发现,a就是我需要的spt的值
但是想单独获取a的值,发现还需要知道t的值
这种情况下,先把断点去掉,刷新页面,然后往下多拉几下,打上断点,运行。发现t就是cend嘛。
万事俱备,参数都知道什么意思了,就可以反解析js,用python去实现这个逻辑。这里重点的代码其实就几行
但是我用了另一个偷懒的办法,直接用了python中execjs这个库,可以在python代码里运行js
def get_spt(start,channel_id):
# start = 10
end = start + 10
n = "/home/q/news_list_for_channel?channel_id=12833307364&cstart=20&cend=30&infinite=true&refresh=1&__from__=pc&multi=5"
e = str(channel_id)
ctx = execjs.compile(
'''
function good (n,e,i,t){
for (var o = "sptoken", a = "", c = 1; c < arguments.length; c++){
o += arguments[c];
}
for (var c = 0; c < o.length; c++) {
var r = 10 ^ o.charCodeAt(c);
a += String.fromCharCode(r)
}
return a
}
'''
)
spt = ctx.call('good', n, e, start, end)
return spt
大概就是这样,spt值会随着id和start的变化而变化,所以这两个参数需要调用的时候传进去。
需要的东西都有了,就开始干正事了
然后就开始爬啊爬爬啊爬,用多线程爬取,直接贴代码吧
import threading
import requests
import time
import execjs
import json
import pymysql
import queue
conn = pymysql.connect("localhost", "root", "", "duanzi")
cursor = conn.cursor()
class ThreadCrawl(threading.Thread):
def __init__(self,pageQ,dataQ,channel_id):
super(ThreadCrawl, self).__init__()
self.pageQ = pageQ
self.channel_id = channel_id
self.dataQ = dataQ
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
'Cookie': 'JSESSIONID=f343b9297c92bebea332b487f2d20983ff3b4a3821f93cf82ce3cce174231e6d; wuid=739422065545486; wuid_createAt=2018-08-22 17:27:56; weather_auth=2; UM_distinctid=16560f64666e-035db1803b62e1-3a614f0b-1fa400-16560f64667a21; Hm_lvt_15fafbae2b9b11d280c79eff3b840e45=1534930077,1534930466,1534949684,1534949904; CNZZDATA1255169715=853053938-1534929958-null%7C1534947899; captcha=s%3A123828459ba61420e936def5c953d459.yR6haqlwGjagUWB7z%2FVR5k3Tx4R8FNeAJrLwtlEiVew; Hm_lpvt_15fafbae2b9b11d280c79eff3b840e45=1534950410; cn_1255169715_dplus=%7B%22distinct_id%22%3A%20%2216560f64666e-035db1803b62e1-3a614f0b-1fa400-16560f64667a21%22%2C%22sp%22%3A%20%7B%22%24_sessionid%22%3A%200%2C%22%24_sessionTime%22%3A%201534950409%2C%22%24dp%22%3A%200%2C%22%24_sessionPVTime%22%3A%201534950409%7D%7D',
# 'Connection': 'keep-alive',
# 'Host': 'www.yidianzixun.com',
'Referer': 'http://www.yidianzixun.com/channel/u12131',
# 'X-Requested-With': 'XMLHttpRequest'
}
def run(self):
self.down()
def down(self):
# print('开始爬取' + threading.current_thread().name)
while not CRAWL_EXIT:
# if self.pageQ.empty():
# break
try:
# 可选参数block,默认值为True
# 1.如果队列为空,block为True的话,不会结束,会进入阻塞状态,直到队列有新的数据
# 2.如果队列为空,block为False的话,就会弹出一个Queue.empty()异常
cstart = self.pageQ.get(False)
#print('cstart {}'.format(cstart))
spt = self.get_spt(cstart)
url = 'http://www.yidianzixun.com/home/q/news_list_for_channel'
t = time.time() * 1000
data = {
'channel_id': self.channel_id,
'cstart': int(cstart),
'cend': int(cstart+10),
'infinite': 'true',
'refresh': 1,
'__from__': 'pc',
'multi': 5,
'_spt': spt,
'appid': 'web_yidian',
'_': t,
}
resp = requests.get(url=url, params=data, headers=self.headers)
py_dict = json.loads(resp.text) # 将json对象转化为python字典
#print(py_dict)
self.dataQ.put(py_dict)
except:
pass
# print('爬取结束' + threading.current_thread().name)
def get_spt(self,cstart):
n = "/home/q/news_list_for_channel?channel_id=12833307364&cstart=20&cend=30&infinite=true&refresh=1&__from__=pc&multi=5"
e = str(self.channel_id)
ctx = execjs.compile(
'''
function good (n,e,i,t){
for (var o = "sptoken", a = "", c = 1; c < arguments.length; c++){
o += arguments[c];
}
for (var c = 0; c < o.length; c++) {
var r = 10 ^ o.charCodeAt(c);
a += String.fromCharCode(r)
}
return a
}
'''
)
spt = ctx.call('good', n, e, cstart, cstart+10)
return spt
class ThreadParse(threading.Thread):
def __init__(self,dataQ,summaryQ):
super(ThreadParse, self).__init__()
self.dataQ = dataQ
self.summaryQ = summaryQ
def run(self):
self.parse()
def parse(self):
while not PARSE_EXIT:
try:
# time.sleep(1)
#print('开始解析' + threading.current_thread().name)
# if self.dataQ.empty():
# break
# 每条content包含了多个summary,也就是多个段子
# 不加这个False参数的话,解析线程都不会退出去,一直等着队列里来数据,阻塞了
content = self.dataQ.get(False)
for i in content['result']:
summary = i.get('summary')
#print(summary)
if summary is None:
break
self.summaryQ.put(summary)
except Exception as e:
print(e)
#print('解析结束' + threading.current_thread().name)
class ThreadSave(threading.Thread):
def __init__(self,summaryQ,lock):
super(ThreadSave, self).__init__()
self.summaryQ = summaryQ
self.lock = lock
def run(self):
self.save()
def save(self):
while not SAVE_EXIT:
# time.sleep(1)
# if self.summaryQ.empty():
# break
# print('开始保存' + threading.current_thread().name)
with self.lock:
try:
summary = self.summaryQ.get(False)
summary = pymysql.escape_string(summary) # 对数据进行预处理,处理数据中的一些特殊字符
sql = 'insert into content(a) VALUES ("%s")' % summary
cursor.execute(sql)
conn.commit()
#print('insert susses')
except Exception as e:
print('insert error')
conn.rollback()
#print('保存结束' + threading.current_thread().name)
CRAWL_EXIT = False
PARSE_EXIT = False
SAVE_EXIT = False
def main():
lock = threading.RLock()
channel_id = ?????
dataQ = queue.Queue() # 存取爬下来的数据
pageQ = queue.Queue() # 存页码
summaryQ = queue.Queue() # 存解析后的数据
for i in range(10):
pageQ.put(i*10)
threadcrawl = []
for i in range(3): # 三个爬取线程
thread = ThreadCrawl(pageQ,dataQ,channel_id)
thread.start()
threadcrawl.append(thread)
threadparse = []
for i in range(3): # 三个解析线程
thread = ThreadParse(dataQ,summaryQ)
thread.start()
threadparse.append(thread)
threadsave = []
for i in range(3): # 三个存储线程
thread = ThreadSave(summaryQ,lock)
thread.start()
threadsave.append(thread)
while not pageQ.empty(): # 页码队列不为空时,让采集线程一直跑着
pass
global CRAWL_EXIT
CRAWL_EXIT = True
for thread in threadcrawl:
thread.join()
print('1')
while not dataQ.empty():
pass
global PARSE_EXIT
PARSE_EXIT = True
for thread in threadparse:
thread.join()
print('2')
while not summaryQ.empty():
pass
global SAVE_EXIT
SAVE_EXIT = True
for thread in threadsave:
thread.join()
print('3')
with lock:
cursor.close()
conn.close()
if __name__ == '__main__':
main()
其实我刚开始观察spt的时候,是这样观察的
然后去掉大家都一样,只留下变化的部分
最后发现一个规律
用这种规律也是可以推出来spt的值,但是这没有破解js来的痛快不是么。