美文网首页
分析Ajax请求并抓取今日头条数据

分析Ajax请求并抓取今日头条数据

作者: 小温侯 | 来源:发表于2018-07-21 00:26 被阅读510次

    思路想法

    AJAX

    先简单扫盲一下什么是AJAX,个人建议如果以下提到的几个名词你都没听说过或者只简单用过,那么你最好还是回头把每一项都补一补,虽然这对写爬虫帮助不大,但是对你深入理解计算机这门学科帮助很大。须知不管是语言,还是框架都有其存在的目的,搞清楚了这些,学起东西来就相对容易了。

    AJAX (Asynchronous JavaScript and XML),直译的话就是异步的JavaScript和XML。实际上,这个名字有一定的误导性:据我所知,AJAX至少还支持json文件,可能是这个技术发明的时候只有JavaScript和XML存在才这样命名的。这里的异步指的是异步加载或者异步数据交换,指的是利用XMLHttpRequst或其他fetch API在网页初步加载结束后,再次发送请求并从服务器上获取并解析数据,然后把这部分数据添加到到已有的页面上,在这个过城中,访问的URL始终没变。

    AJAX本质上是一种框架,通过JavaScript可以实现部分更新网页的效果。这样可以节省互联网中的传输带宽。JavaScript是一种浏览器脚本语言,它可以通过不访问服务器达到修改客户端的目的。比如说,有的网站有很多选项,当你选了其中一个选项,会出现更多的子选项,这其实就是Javascript做的,直到你点了提交,浏览器才会和服务器交互。AJAX框架里用到的只是JavaScript众多功能的一小部分。

    今日头条

    Hmm...其实我也不喜欢今日头条,推送的内容越来越像UC头条了。如果你在头条页面上搜索关键词古镇,你会得到若干文章,如果你一直下拉滑动条到底部,你会发现浏览器会自动加载更多的文章,这就是AJAX的一种表现。

    然而,万变不离其宗,不管是什么技术,框架,只要HTTP协议仍在使用,都跑不过GET/POST和其响应,我们要做的就是找到这条关键的GET请求。

    有没有用AJAX

    判断一个网站是不是用了AJAX可以从以下几点看出端倪:

    • 最实锤的,就是用浏览器审查元素的时候,在‘Network’里过滤XHR标签。XHR应该就是XMLHttpRequest这是AJXAX的一个特征。
    • 像今日头条这样的网站会有明显的异步加载现象(就是往下拖的时候会出现新的内容)。
    • 查看网页源代码,注意不是审查元素。审查是看不出来的,因为有可能异步加载已经完成了,你也许可以从审查元素里找到你要的HTML结构,但它们不是requests返回的而是javascript加载的。
    • 如果一个网页源码里充斥这各种JavaScript,而html代码很少,这种也很有可能是AJAX。

    综上,其实还是很容易看出来的。

    分析搜索页面

    toutiao-索引页分析.jpg

    如图中1位置过滤XHR标签,会在位置2看到其报文。第一次访问时只有一条,随着进度条下拉,会不断出现新的请求。分析这四个报文,你可以在它的request的Header中找到参数列表,我们只需要修改其参数值就行了,其他参数也可以改,其中keyword就是你搜索的关键词。

    Requests URLs(只有offset变化了):
    https://www.toutiao.com/search_content/?
    offset=0&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
    https://www.toutiao.com/search_content/?
    offset=20&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
    https://www.toutiao.com/search_content/?
    offset=40&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
    ......
    

    这样我们就有了要访问的链接,随着offset=0, 20, 40, 60...增加,每条请求都会返回如上图中位置3的一个json结构,里面包含了不超过20条记录。如果json结构中data项目为空,说明已经没有结果了,标识爬虫结束。有时候搜索结果会很多很多,也可以设一个上限,比如说最多爬去前50个搜索结果。

    toutiao-两种类型.jpg

    仔细看的话,搜索结果会有两种,一种是文字+图片的结构,包含图片和文字;另一种就是相册形式的结构,会包含若干张照片。这两种结果可以根据json结构里has_galleryhas_image字段来区分。

    每条结果对应的URL我选用的是article_url字段的值,但其实这里有个问题,再后来的爬取过程中,我发现这个字段的值并不全是头条的文章,有的是文章的源地址,估计是头条从网上爬来在规整加到自己的结果里的,一般出现在前几个。这种情况后面写的对应的爬取代码就不适用了,谁知道文章源地址用了什么样的结构。不过考虑到这种情况不多,我就简单过滤了一下,反正少爬几个页面问题也不大。

    如果有强迫症,我考虑的一个方法是采用sourcs_url字段的内容,但是这个地址会被重定向,所以get请求要调整一下:

    https://www.toutiao.com/group/6544161999004107271/
    Redirect to:
    https://www.toutiao.com/a6544161999004107271/
    

    带图集的页面

    toutiao-带图集.jpg

    如图就是这类页面中的图集,我们要把这12张图片地址找出来。因为已经确定用了AJAX,这里要查看其网页源代码,之后就能找到:

    toutiao-带图集-js.jpg

    内容有时会略显不同,但是是很容易找的。之后用正则表达式把你要的图片地址过滤出来就行。这里有个技巧,就是这部分的源码正好是json格式,你就可以把整个{}过滤出来之后load成一个字典,便于访问。

    不带图集的页面

    不带图集的页面就是文字+图片的模式,不过头条的这种页面也是动态加载的,我一开始以为这是静态页面,看了源代码才知道。

    toutiao-不带图集-js.jpg

    找图片地址也很容易,仍然用正则表达式把它们过滤出来。

    代码

    代码结构

    1. 自建URL,调整offset和keyword取回索引页面,分析json.get('data')的结构,区分两类页面,将提取的URL放到各自对应的list中。
    2. 两个解析函数,分别用来解析两种页面,并把提取出的图片地址写入文件。

    几个小问题:

    • 我发现即使是同样的内容,正则会有时有结果有时没结果,不知道为什么?正则库不靠谱?这里如果正则没结果会被舍弃掉,并打印一条错误信息,程序继续。
    • 关于处理带图集页面的函数中使用的正则式'gallery: JSON\.parse\("(.*?)max_img',这里的.之前也要转义符。虽然.代表了任意字符,这里加不加结果不会变。但是如果内容里还有如JSON?parse这样的表达式,不带转义符是过滤不出来的。
    • 这个代码非常适合用多线程,一条线程爬,一条处理带图集的页面,一条处理不带图集的页面,处理的时候对两个list上锁。(我代码里没实现多线程)

    jinritoutiao.py

    configure.py请参考拙作:爬取糗事百科的内容和图片并展示

    import requests
    import json
    import time
    import re
    from random import choice
    import configure
    
    url = "https://www.toutiao.com/search_content/?"
    header = {'user-agent': choice(configure.FakeUserAgents)}
    keyword = '塞尔达传说'
    
    has_gallery_lists = []
    no_gallery_lists = []
    
    def SearchPageParser(offset = 0):
        payload = {
            'offset':offset,
            'format':'json',
            'keyword':keyword,
            'autoload':'true',
            'count':30,
            'cur_tab':1,
            'from':'search_tab'
        }
    
        count = 0
    
        try:    
            response = requests.get(url, headers=header, params=payload)
            content = None
    
            print ("Parser " + response.url)
            if response.status_code == requests.codes.ok:
                content = response.text
                data = json.loads(content)
    
                if not data:
                    return
    
                for article in data.get('data'):
                    if True == article.get('has_gallery') and True == article.get('has_image'):
                        has_gallery_lists.append(article.get('article_url'))
                        count += 1
    
                    if False == article.get('has_gallery') and True == article.get('has_image'):
                        no_gallery_lists.append(article.get('article_url'))
                        count += 1
    
                return count    
    
        except Exception as e:
            print (e)
            return
    
    def SaveImage(imageURL):
        # 这里就不下载了,只是把单纯写入文件
        print (imageURL)
    
        with open('toutiao.txt', 'a') as file:
            file.write(imageURL + '\n')
    
    
    def HasGalleryParser():
        if 0 == len(has_gallery_lists):
            return
    
        # 这里写的时候注意(, ), ", ., 都是要转义的。
        pattern = re.compile('gallery: JSON\.parse\("(.*?)max_img', re.S)
    
        while has_gallery_lists:
            this = has_gallery_lists.pop()
    
            try:    
                response = requests.get(this, headers=header)
                content = None
    
                if response.status_code == requests.codes.ok:
                    content = response.text
                    data = pattern.findall(content)
    
                    if data:
                        data = data[0][:-4].replace('\\','') + ']}'
                        img_urls = json.loads(data).get('sub_images')
    
                        for img_url in img_urls: 
                            SaveImage(img_url.get('url'))
                    else:
                        print ("BadPageURL[GalleryParser, {0:s}]".format(this))
    
            except Exception as e:
                print (e)
                return
    
        time.sleep(0.25)
    
    def NoGalleryParser():
        if 0 == len(no_gallery_lists):
            return
    
        while no_gallery_lists:
            this = no_gallery_lists.pop()
    
            pattern = re.compile('<img src="(.*?)"', re.S)
            try:    
                response = requests.get(this, headers=header)
                content = None
    
                if response.status_code == requests.codes.ok:
                    content = response.text
                    img_urls = pattern.findall(content)
                    
                    if img_urls:
                        for img_url in img_urls: 
                            SaveImage(img_url)
                    else:
                        print ("BadPageURL[NoGalleryParser, {0:s}]".format(this))
    
            except Exception as e:
                print (e)
                return
    
        time.sleep(0.25)
    
    
    
    if __name__ == "__main__":
        x, count = 0, 0
    
        cnt_urls = SearchPageParser(x)
    
        while count < 20 and cnt_urls:
            cnt_urls = SearchPageParser(x+20)
            count += cnt_urls
            x += 20
            time.sleep(0.55)
    
        print ("Get {0:d} URL(s) in total.".format(count))
    
        HasGalleryParser()
        NoGalleryParser()
    

    相关文章

      网友评论

          本文标题:分析Ajax请求并抓取今日头条数据

          本文链接:https://www.haomeiwen.com/subject/wzycmftx.html