分析Ajax爬取今日头条街拍美图-崔庆才思路

站点分析

首先,打开头条,在搜索框输入关键字以后,在返回的页面中,勾选Perserve log,这玩意儿在页面发生变化的时候,不会清除以前的交互信息.html

在返回的response中,咱们看不到常见的HTML代码,因此初步断定,这个网站是经过ajax动态加载的.java


pic-1581682361199.png

切换到XHR过滤器,进一步查看.python


pic-1581682361200.png

发现随着网页的滚动,会产生相似这样的的Ajax请求出来. 仔细查看内容,能够看到与网页中条目对应的title和article_url.web

因此初步思路,经过article_url字段先抓取文章条目
ajax

分析json数据,能够看到,这里有article_url,另外,此次要抓取的是图集形式的页面,因此要注意下这个has_gallerymongodb

而后咱们再来看具体的页面数据库

在具体页面的html中,咱们发现,图片的全部连接直接在网页源代码中包含了,因此,咱们直接拿到源码,正则匹配一下就行了.json


pic-1581682361200.png

至此,页面分析完成.api

开工!cookie

源码及遇到的问题

代码结构

方法定义

def get_page_index(offset, keyword): 获取搜索结果索引页面
def parse_page_index(html): 解析索引页面,主要是解析json内容,因此须要用到json.loads方法
def get_page_detail(url): 用来获取具体图片的页面,与索引页获取差很少
def parse_page_details(html, url):解析具体图集页面
def save_to_mongo(result): 将标题,url等内容保存到mongoDB数据库. 之因此使用mongoDB数据库,由于mongoDB简单,并且是K-V方式的存储,对于字典类型很友好
def download_image(url): 下载图片
def save_img(content): 保存图片
def main(offset): 对以上各类方法的调用

须要的常量

MONGO_URL = 'localhost' # 数据库位置
MONGO_DB = 'toutiao'    # 数据库名
MONGO_TABLE = 'toutiao'# 表名
GROUP_START = 1 # 循环起始值
GROUP_END = 20 # 循环结束值
KEY_WORD = '街拍' # 搜索关键字

关于在代码中遇到的问题

01. 数据库链接

第一次在python中使用数据库,并且用的仍是MongoDB. 使用以前引入 pymongo库,数据库链接的写法比较简单. 传入url 而后在建立的client中直接指定数据库名称就能够了.

client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]

02.今日头条的反爬虫机制

今日头条比较有意思,反爬虫机制不是直接给个400的回应,而是返回一些错误的 无效的代码或者json. 不明白是什么原理,是请求不对,仍是怎么了. 因此针对今日头条的反爬虫机制,通过尝试以后发现须要构造get的参数和请求头.
并且今日头条的请求头中,须要带上cookie信息. 否则返回的response仍是有问题.

这里还要注意的就是cookie信息有时效问题,具体多长时间,我也没搞明白,几个小时应该是有的,因此在执行以前,cookie最好更新一下

一样的在获取详情页的时候也有这个问题存在. 并且还犯了一个被本身蠢哭的错误. headers没有传到requests方法中去.

def get_page_index(offset, keyword):
    timestamp = int(time.time())
    data = {
        "aid": "24",
        "app_name": "web_search",
        "offset": offset,
        "format": "json",
        "keyword": keyword,
        "autoload": "true",
        "count": "20",
        "en_qc": "1",
        "cur_tab": "1",
        "from": "search_tab",
        # "pd": "synthesis",
        "timestamp": timestamp
    }
    headers = {
        # 这里当心cookie失效的问题
        'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=ngww6x1t11581323903383',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
    url = 'https://www.toutiao.com/api/search/content/?' + urlencode(data)
    response = requests.get(url, headers=headers)
    try:
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print('Request failed!')
        return None

03. json解码遇到的问题

因为python和java转移字符的区别(python经过''进行转义,''自己不须要转义),可是java须要\\来进行转义,也就是''自己还须要一个''来进行转义.

可是python的json.loads()方法和print方法在输出的时候都会对转义字符进行解释.
因此当初在parse_page_details()这个方法中 json.loads()报错,说json格式错误找不到'"'. 可是print出来的时候,又是一个''的样子.

后来在在debug的时候,看到了真实的json字符串的样子

因此就须要对这个json字符串进行预处理,而后再使用json.loads()进行解码.

eval(repr(result.group(1)).replace('\\\\', '\\'))

插一个小话题,那就是str()方法和repr()方法的区别. 首先二者都是把对象转换成字符串,而不管print方法仍是str()方法调用的都是类中的__str__ 而repr()方法调用的是__repr__ .
简单来讲,__str__方法是为了知足可读性,会对输出内容作可读性处理. 好比去掉字符串两端的引号或者自动解析''等. 可是__repr__会尽可能保留原始数据格式,知足的是准确性需求. 因此这里,咱们使用repr()方法拿到原始数据,而后将\\ 替换为\

ps.\\\\ 是两个\ 转义了一下. 同理两个斜杠是一个斜杠,由于也是转义的.

而后就是eval方法是能把字符串转换成对应的类型.

#字符串转换成列表
 >>>a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
 >>>type(a)
 <type 'str'>
 >>> b = eval(a)
 >>> print b
 [[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
 >>> type(b)
 <type 'list'>
#字符串转换成字典
>>> a = "{1: 'a', 2: 'b'}"
>>> type(a)<type 'str'
>>>> b = eval(a)
>>> print b
{1: 'a', 2: 'b'}>>> type(b)<type 'dict'>

理解repr()和eval()两个方法以后,那上面的预处理代码就好理解了,先经过repr()方法获取原始字符串,而后替换,而后再给他转换成可读的字符串. 而后在用json.loads()解码.

04. 关于response.text和response.content的区别

response.text 获取文本值
response.content 获取二进制内容

源代码

import json
import os
import re
from hashlib import md5
from multiprocessing import Pool
from urllib.parse import urlencode
import pymongo
import requests
from bs4 import BeautifulSoup
from requests.exceptions import RequestException
from config import *

# mongodb 数据库对象
# connext=False表示进程启动的时候才进行链接
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]

def get_page_index(offset, keyword):
    data = {
        "aid": "24",
        "app_name": "web_search",
        "offset": offset,
        "format": "json",
        "keyword": keyword,
        "autoload": "true",
        "count": "20",
        "en_qc": "1",
        "cur_tab": "1",
        "from": "search_tab",
        # "pd": "synthesis",
        # "timestamp": "1581315480994"
    }
    headers = {
        # 这里当心cookie失效的问题
        'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=ngww6x1t11581323903383',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
    url = 'https://www.toutiao.com/api/search/content/?' + urlencode(data)
    response = requests.get(url, headers=headers)
    try:
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print('Request failed!')
        return None

def parse_page_index(html):
    data = json.loads(html)
    # json.loads()方法会格式化结果,并生成一个字典类型
    # print(data)
    # print(type(data))
    try:
        if data and 'data' in data.keys():
            for item in data.get('data'):
                if item.get('has_gallery'):
                    yield item.get('article_url')
    except TypeError:
        pass

def get_page_detail(url):
    headers = {
        'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=yix51k4j41581315307695',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36',
        # ':scheme': 'https',
        # 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        # 'accept-encoding': 'gzip, deflate, br',
        # 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7'
    }
    try:
        # 他妈的被本身蠢哭...忘了写headers了,搞了一个多小时
        response = requests.get(url, headers=headers)
        # print(response.status_code)
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print("请求详情页出错!")
        return None

def parse_page_details(html, url):
    soup = BeautifulSoup(html, 'xml')
    title = soup.select('title')[0].get_text()
    # print(title)
    img_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)
    result = re.search(img_pattern, html)
    if result:
        # 这里注意一下双斜杠的问题
        data = json.loads(eval(repr(result.group(1)).replace('\\\\', '\\')))
        if data and 'sub_images' in data.keys():
            sub_images = data.get('sub_images')
            images = [item.get('url') for item in sub_images]
            for image in images: download_image(image)
            return {
                'title': title,
                'url': url,
                'images': images
            }

def save_to_mongo(result):
    if db[MONGO_TABLE].insert_one(result):
        print('存储到MongoDB成功', result)
        return True
    return False

def download_image(url):
    print('正在下载', url)
    try:
        response = requests.get(url)
        if response.status_code == 200:
            save_img(response.content)
        return None
    except RequestException:
        print('请求图片出错', url)
        return None

def save_img(content):
    file_path = '{0}/img_download/{1}.{2}'.format(os.getcwd(), md5(content).hexdigest(), 'jpg')
    if not os.path.exists(file_path):
        with open(file_path, 'wb') as f:
            f.write(content)
            f.close()

def main(offset):
    html = get_page_index(offset, KEY_WORD)
    for url in parse_page_index(html):
        html = get_page_detail(url)
        if html:
            result = parse_page_details(html, url)
            if result: save_to_mongo(result)

if __name__ == '__main__':
    groups = [x * 20 for x in range(GROUP_START, GROUP_END + 1)]
    pool = Pool()
    pool.map(main, groups)
相关文章
相关标签/搜索