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

思路想法

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()
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容