多线程爬取一点资讯

首先观察网站,明确爬取目标

经过观察,发现这个网站的数据都是异步加载的,而我此次爬取的目标,是一点资讯-段子模块下的内容

分析目标站点

既然是异步加载的,就可以在控制台的 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来的痛快不是么。

猜你喜欢

转载自blog.csdn.net/fiery_heart/article/details/82025680
今日推荐