python爬取今日头条街拍美图

爬取街拍美图(注意:以下长文预警)

成品展示

下图是街拍美图保存到本地的电脑截图。
在这里插入图片描述
下图是程序运行时的截图。
在这里插入图片描述

需求分析

首先,打开头条的街拍页面,我在不断的往下滑动,页面一直有新的标签刷出来,不过页面的 url 斌并没有变化,所以我猜测这是通过ajax加载的。如下图所示。
在这里插入图片描述

上面,我画了三个圈,分别表示三种类型。第一种是点进去之后,必须得通过点击才能看到下一张图片;第二钟是点进去之后,直接往下滑动就可以看到全部的图片,第三种是视频,不在本篇博文的讨论范围。第一和第二种类型都是通过JavaScript加载的页面。

获取索引页

通过查看多个ajxa请求之后,发现只有offset是变化的,并且变化规律很明显。还有,keyword参数其实就是搜索的关键字。

def get_index_page(offset, keyword):
    params = {
        'offset': offset,
        'format': 'json',
        'keyword': keyword,
        'autoload': 'true',
        'count': '20',
        'cur_tab': '1',
        'from': 'search_tab',
        'pd': 'synthesis'
    }
    base_url = 'https://www.toutiao.com/api/search/content/?'
    url = base_url + urlencode(params)
    try:
        response = requests.get(url=url, headers=headers)
        if response.status_code == codes.ok:
            print(url + ':导航页请求成功!')
            return response.text
    except requests.ConnectionError:
        print('请求导航页失败!')
        return None

我把参数放在一个字典中,然后用urlencode函数封装属性。然后,拼接两个字符串。

解析索引页内容

分析ajax请求的返回结果,拿到详情页的url。在这里,我发现每个data里面的第一项里面都含有key为cell_type的键值对,而且这个选项没有url,所以这里有两行去除的语句。函数返回的是详情页的url列表。

def parse_index_page(html):
    data = json.loads(html)
    if data and 'data' in data.keys():
        for item in data.get('data'):
            if item.get('cell_type') is not None:  
                continue
            yield item.get('article_url')

根据索引页的url获取详情页

根据URL的链接,逐个请求详情页,并返回详情页的内容。

def get_detail_page(url):
    try:
        response = requests.get(url=url, headers=headers)
        if response.status_code == 200:
            print(url + ':详情页请求成功!')
            return response.text
        else:
            return None
    except RequestException:
        print('请求详情页错误', url)
        return None

解析详情页的内容

通过查看详情页返回的内容可以发现,详情页都是通过JavaScript加载的,而且有三种类型,在下面的函数中,我对其中的两种做了提取工作。第三种没有理会,因为它是视频,不在讨论范围内。

def parse_detail_page(html, url):
    try:
        soup = BeautifulSoup(html, 'lxml')
        title = soup.select('title')[0].get_text()  # 获取文章title
        images_pattern_1 = re.compile('gallery: JSON.parse\("(.*?)"\)', re.S)  # 匹配模式
        result_1 = re.search(images_pattern_1, html)  # 匹配内容
        images_pattern_2 = re.compile('img src="(.*?)"', re.S)
        result_2 = re.findall(images_pattern_2, html) 
        if result_1:  # 第一种网页:点击才能跳转图片
            str = re.sub(r'(\\)', '', result_1.group(1))  # 去掉url链接中多余的双斜线“\\”
            if str:  # 如果匹配到内容,执行接下来的操作
                data = json.loads(str)
                if data and 'sub_images' in data.keys():  
                    sub_images = data.get('sub_images')
                    images = [item.get('url') for item in sub_images]  # 提取sub_images中图片的url链接
                    yield {
                        'title': title,  # 详情页标题
                        'url': url,  # 详情页链接
                        'images': images  # 图片链接
                    }
        elif result_2:  # 第二种网页: 往下滑动就能看到全部图片
            # reulut_2返回的就是一个列表
            yield {
                'title': title,  # 详情页标题
                'url': url,  # 详情页链接
                'images': result_2  # 图片链接
            }
    except:
        return None  # 跳过异常继续执行

下载图片(下载+存图)

通过URL下载图片,并把二进制文件传到保存图片的函数中。

def download_img(image_url):
    print('正在下载:', image_url)
    try:
        response = requests.get(image_url, headers=headers)
        if response.status_code == 200:
            save_to_local_file(response.content)  
        else:
            return None
    except RequestException:
        print('下载图片错误:', image_url)
        return None

存储图片方法

把图片保存到当前文件夹的img文件夹中。

def save_to_local_file(content):
    img_path = 'img'
    if not os.path.exists(img_path):
        os.makedirs(img_path)
    file_path = img_path + os.path.sep + '{file_name}.{file_suffix}'.format(
        file_name=md5(content).hexdigest(),
        file_suffix='jpg')
    with open(file_path, 'wb') as f:
        f.write(content)

定义主函数,调用之前的方法

定义一个mian函数,调度上面的所有函数。

def main(offset):
    html = get_index_page(offset=offset, keyword=keyword)
    for url in parse_index_page(html):  # 返回的是一个迭代器,每次输出一个网址
        html = get_detail_page(url)
        if html:
            result = parse_detail_page(html, url)  # 传入详情页链接、详情页内容,进行解析
            print(result)
            for item in parse_detail_page(html, url):
                image_list = item.get('images')  # 传入图片链接,下载并保存到本地
                for image in image_list:
                    download_img(image)

只运行本文件中的主函数

这里是程序的入口,在这里建立了一个进程池,加快爬取的效率。

if __name__ == '__main__':
    groups = [i * 20 for i in list(range(GROUP_START, GROUP_END))]  
    pool = Pool()  # 创建进程池
    pool.map(main, groups) 
    pool.close()
    pool.join()

全局变量的设置

设置全局变量的意义是实现程序的可配置性。虽然不是全部可配置,但是也算是使得代码更加灵活了以及可重用性更加高了。我在这里设置了偏移量和请求头。

offset = '0'
keyword = '街拍图片'

GROUP_START = 1
GROUP_END = 20

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64)",
    "Referer": "https://www.toutiao.com/"
}

总结和提高

这次的爬取代码,我利用课余的时间写了两天,最主要的时间都是耗在程序的架构以及提取上面了。我对正则表达式的掌握程度还不够,不能够熟练的运用。
接下来,我要改进一下保存图片写方式,比如说吧=把图片的链接存储到MySQL数据中,以及在本地保存时要按照每一个详情页一个文件夹这样分开存储,方便我查看图片。

完整代码

__author__ = 'Py.ziMing'
import json
import os
import re
from hashlib import md5
from multiprocessing.pool import Pool
from urllib.parse import urlencode

import requests
from bs4 import BeautifulSoup
from requests import codes, RequestException

offset = '0'
keyword = '街拍图片'

GROUP_START = 1
GROUP_END = 20

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64)",
    "Referer": "https://www.toutiao.com/"
}


# 获取索引页:
def get_index_page(offset, keyword):
    params = {
        'offset': offset,
        'format': 'json',
        'keyword': keyword,
        'autoload': 'true',
        'count': '20',
        'cur_tab': '1',
        'from': 'search_tab',
        'pd': 'synthesis'
    }
    base_url = 'https://www.toutiao.com/api/search/content/?'
    url = base_url + urlencode(params)
    try:
        response = requests.get(url=url, headers=headers)
        if response.status_code == codes.ok:
            print(url + ':导航页请求成功!')
            return response.text
    except requests.ConnectionError:
        print('请求导航页失败!')
        return None


# 解析索引页内容
# 分析ajax请求的返回结果,拿到详情页的url
def parse_index_page(html):
    data = json.loads(html)
    if data and 'data' in data.keys():
        for item in data.get('data'):
            if item.get('cell_type') is not None:  # 去除没有url的item
                continue
            yield item.get('article_url')


# 根据索引页的url获取详情页
def get_detail_page(url):
    try:
        response = requests.get(url=url, headers=headers)
        if response.status_code == 200:
            print(url + ':详情页请求成功!')
            return response.text
        else:
            return None
    except RequestException:
        print('请求详情页错误', url)
        return None


# 解析详情页的内容
def parse_detail_page(html, url):
    try:
        soup = BeautifulSoup(html, 'lxml')
        title = soup.select('title')[0].get_text()  # 获取文章title
        images_pattern_1 = re.compile('gallery: JSON.parse\("(.*?)"\)', re.S)  # 匹配模式
        result_1 = re.search(images_pattern_1, html)  # 匹配内容
        images_pattern_2 = re.compile('img src="(.*?)"', re.S)
        result_2 = re.findall(images_pattern_2, html)  # 匹配内容
        if result_1:  # 第一种网页:点击才能跳转图片
            str = re.sub(r'(\\)', '', result_1.group(1))  # 去掉url链接中多余的双斜线“\\”
            if str:  # 如果匹配到内容,执行接下来的操作
                data = json.loads(str)
                if data and 'sub_images' in data.keys():  # 确保返回的信息中含有sub_images这个信息
                    sub_images = data.get('sub_images')
                    images = [item.get('url') for item in sub_images]  # 提取sub_images中图片的url链接
                    yield {
                        'title': title,  # 详情页标题
                        'url': url,  # 详情页链接
                        'images': images  # 图片链接
                    }
        elif result_2:  # 第二种网页: 往下滑动就能看到全部图片
            # reulut_2返回的就是一个列表
            yield {
                'title': title,  # 详情页标题
                'url': url,  # 详情页链接
                'images': result_2  # 图片链接
            }
    except:
        return None  # 跳过异常继续执行


# 下载图片(下载+存图)
def download_img(image_url):
    print('正在下载:', image_url)
    try:
        response = requests.get(image_url, headers=headers)
        if response.status_code == 200:
            save_to_local_file(response.content)  # content返回二进制内容,图片一般返回content
        else:
            return None
    except RequestException:
        print('下载图片错误:', image_url)
        return None


# 存储图片方法
def save_to_local_file(content):
    img_path = 'img'
    if not os.path.exists(img_path):
        os.makedirs(img_path)
    file_path = img_path + os.path.sep + '{file_name}.{file_suffix}'.format(
        file_name=md5(content).hexdigest(),
        file_suffix='jpg')
    with open(file_path, 'wb') as f:
        f.write(content)


# 定义主函数,调用之前的方法
def main(offset):
    html = get_index_page(offset=offset, keyword=keyword)
    for url in parse_index_page(html):  # 返回的是一个迭代器,每次输出一个网址
        html = get_detail_page(url)
        if html:
            result = parse_detail_page(html, url)  # 传入详情页链接、详情页内容,进行解析
            print(result)
            for item in parse_detail_page(html, url):
                image_list = item.get('images')  # 传入图片链接,下载并保存到本地
                for image in image_list:
                    download_img(image)


# 只运行本文件中的主函数
if __name__ == '__main__':
    groups = [i * 20 for i in list(range(GROUP_START, GROUP_END))]  # python3 range()不能直接生成列表,需要list一下
    pool = Pool()  # 创建进程池
    pool.map(main, groups)  # 第一个参数是函数,第二个参数是一个迭代器,将迭代器中的数字作为参数依次传入函数中
    pool.close()
    pool.join()

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值