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()
上一篇: ubuntu下texmaker安装及配置