分析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()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,729评论 6 517
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,226评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 169,461评论 0 362
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,135评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,130评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,736评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,179评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,124评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,657评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,723评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,872评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,533评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,213评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,700评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,819评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,304评论 3 379
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,876评论 2 361

推荐阅读更多精彩内容