欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python爬取今日头条街拍美图

程序员文章站 2022-05-26 17:49:21
...

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

成品展示

下图是街拍美图保存到本地的电脑截图。
python爬取今日头条街拍美图
下图是程序运行时的截图。
python爬取今日头条街拍美图

需求分析

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

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