本文是学习 天善学院 Python3爬虫三大案例实战分享 / 分析Ajax抓取今日头条街拍美图 后所写,感谢崔庆才崔老师。
天善学院 Python爬虫三大案例实战分享
抓取的是街拍中的图集标签下的内容
流程:
1. 抓取索引页
2. 抓取详情页内容
3. 保存信息到数据库
4. 下载图片
-
索引页
索引页就是这个页面
这个页面就是用ajax来生成的。往下拉动页面,达到边界的时候,Ajax就会自动加载下一页。
鼠标选定的那个就是其中一页索引页,一共加载了四页。
我们可以看到 四个索引页之间的区别只有 offset参数有区别,其他都是一模一样的。
如此来说我们就可以通过修改offset参数来获取任意索引页了。
第一页索引页 offset = 0
第二页索引页 offset = 20
第三页索引页 offset = 40
第四页索引页 offset = 60
由此可以看出 offset的规律是 跨步20递增。
def get_page_index(offset=0):
"""获得索引页内容
:return 返回json格式数据"""
url = 'http://www.toutiao.com/search_content/?format=json&keyword=街拍&autoload=true&count=20&cur_tab=3'
headers = {
'Host':"www.toutiao.com",
'Referer':"http://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D"
}
params = {'offset': offset}
html = downloader(url,headers,params)
return html
视频里 用的是 urllib.urlencode()对url参数进行编码,因为我们使用的是requests库,requests可以自动对参数进行编码,所以没必要在使用urllib.urlencode()
downloader()是抽象出来的下载功能函数。
def downloader(url,headers=None,params=None,again_num=3,isBinary=False):
"""根据url下载html
requests 并不会主动抛出http 状态码异常,
我们只处理服务器异常也就是状态码为500与600之间。服务器异常则重试3次下载
服务器异常的情况下主动抛出requs状态码异常
对超时异常,同样进行重试3次下载
:param again_num 下载异常时,重复下载次数
:param isBinary:是否下载二进制数据,例如图片 True表示下载二进制数据,Flase表示 下载普通html页面
:return 返回html源码页面"""
html = None
try:
response = requests.get(url, headers=headers, params=params)
code = response.status_code
if code == 200:
html = response.content if isBinary else response.text
elif 500 <= code < 600:
response.raise_for_status()
except requests.HTTPError as e :
if again_num > 0:
print ' >>> 服务器异常,下载重试 '
return downloader(url,headers=headers,params=params,again_num=again_num-1)
except requests.Timeout as e:
if again_num > 0:
print ' >>> 超时异常,下载重试 '
return downloader(url,headers=headers,params=params,again_num=again_num-1)
except requests.ConnectionError as e:
print ' >>> 网咯问题,下载失败 --- ',url
return html```
解析索引页,从索引页中提取出来详情页url
Ajax返回的索引页是一个json格式数据。详情页url就包含在json中
![image.png](http://upload-images.jianshu.io/upload_images/4131789-61c8213c9a1f40ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
data字段值就是 详情页url集合
![image.png](http://upload-images.jianshu.io/upload_images/4131789-80f1154bda01629f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
acticle_url字段值 就是 详情页url。
其实 url字段值也是 详情页url 两个是一样的 提取那个都行
![image.png](http://upload-images.jianshu.io/upload_images/4131789-c898c2facc6968d6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
def parse_page_index(html):
"""解析索引页内容,获得新闻url
:raise KeyError 当没有数据时候抛出KeyError异常
:return 新闻url"""
data = json.loads(html)
#这个地方要加一个 data['data']
# 因为data['data']为空,表示索引页已经到底,没有下一页了。
# 其实下一页还可以继续访问,只是返回的全部是重复数据。
if data and ('data' in data) and data['data']:
for item in data['data']:
yield item['url']
else:
raise KeyError(u'没有数据了')
我发现当索引页到底的时候也就是最后一页,在网下一页的时候返回的json数据中 data字段值是一个空数据,表示已经到底了。其实还可以在往下一页继续访问,但是返回的json中的data字段值是前面也已经出现过的重复详情页数据。所以我以 data字段值为空数组表示已经到最后一页,不在进行下一页访问,避免获得重复数据。
视频中用 data.keys()来获得字典的键,其实不用使用data.keys()直接使用 for item in data即可,直接获取的就是 字典的键
2. 抓取详情页
根据详情页url获得详情页html
def get_page_detail(url):
"""获得详情页I新闻页)html源码
:return 详情页html源码"""
headers = {
'Host': "www.toutiao.com",
'Referer': "http://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D"
}
html = downloader(url,headers=headers)
return html
解析详情页,抽取标题,图片urls
图片的urls是页面中用JavaScript写在html页面中的。这个gallery变量中的 sub_images标签的值就是图片集合。
所以我们需要从页面中提取出来gallery
![image.png](http://upload-images.jianshu.io/upload_images/4131789-e40d70159a7a48cc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
def parse_page_detail(html,url):
"""解析详情页,抽取标题,图片url
:return {url,title,urls} 详情页信息 字典集合"""
soup = BeautifulSoup(html,'lxml')
title = soup.title.string
images_pattern = re.compile('var gallery =.?(.?);',re.S)
result = re.search(images_pattern, html)
if result:
data = json.loads(result.group(1))
if data and ('sub_images' in data):
sub_images = data['sub_images']
images = [item['url'] for item in sub_images]
# 放到这里回导致重复下载图片,所以 移动到parse_detail中
# 只有插入数据库成功,才会下载图片,避免因重新运行程序导致对已下载过的图片进行重复的下载。
# for image_url in images:
# download_image(image_url)
return {
'url':url,
'title':title,
'images':images
}
使用正则表达式从页面中提取出gallery的值,并转化为json格式数据,从而提取出图片集合sub_images的值。
在视频中 下载图片是写在这里的。我个人觉得逻辑上有些不合理。
因为:假如,我们一小时前运行完程序下载图片。一小时后我们再次运行程序下载图片,然而网站数据没有变化仍然是一小时前的数据,或者混杂的有一小时前的数据,那么程序任然会重复的对图片进行下载,并不管图片是否已经下载过,这样就造成了没必要的重复下载。我个人认为,只有详情页信息插入数据库成功,才进行图片下载。因为插入成功表示当前详情页url并未访问过,没访问过就表示图片没下载过。
3. 保存到数据库
我们将数据保存到 mongodb数据库
def save_db(result):
"""保存数据到mongodb中
:return 插入数据库成功返回True,否则返回Flase"""
status = False
try:
if db_table.insert(result):
print '保存数据库成功 ',result
status = True
except pymongo.errors.DuplicateKeyError:
# 对唯一字段进行重复插入,pymongo则会抛出这个错误,并且插入失败
print '重复插入'
pass
return status
对已插入过的mongodb文档,进行重复插入会抛出 DuplicateKeyError异常,并插入失败。所有我们捕获这个异常,避免因重复数据的插入导致程序崩溃。
4. 下载图片
下载图片并保存到当前目录下的 images文件夹内
下载图片
def download_image(url):
"""下载图片并保存到当前文件夹"""
image = downloader(url,isBinary=True)
save_image(image)
print '下载图片成功 ---- ',url
保存图片
def save_image(image):
"""保存图片到当前目录下的images文件夹内"""
image_file = 'images'
image_name = hashlib.md5(image).hexdigest()
# 文件后缀
# imghdr.what(f)返回图片文件的格式
# 只接受文件对象,所以用StringIO包装一下
f = StringIO.StringIO(image)
image_suffix = imghdr.what(f)
try:
if not os.path.exists('images'):
os.mkdir('images')
file_path = '{0}{1}{2}.{3}'.format(image_file, os.path.sep, image_name, image_suffix)
with open(file_path,'wb') as f:
f.write(image)
except IOError as e:
print '文件操作失败 --- ',e
使用imghdr.what(file)来获得图片的格式(jpg,gif等)
imghdr.what(file)接受的是 文件对象,所以我们用StringIO包装下载的图片二进制数据,在内存中生成一个文件对象。
4. 主程序
def parse_index(html):
"""解析索引页"""
for url in parse_page_index(html):
page_html = get_page_detail(url)
if page_html:
parse_detail(page_html,url)
def parse_detail(html,url):
"""解析详情页"""
result = parse_page_detail(html, url)
stauts = save_db(result)
# 当数据库插入成功的情况下 进行图片下载,避免重复访问导致的重复图片下载
if stauts:
for image_url in result['images']:
download_image(image_url)
def main(offset):
#parse_index 与 parse_detail 是因为main嵌套层次太多(5层)所以拆分的。
try:
html = get_page_index(offset)
if html:
parse_index(html)
except KeyError as e:
print e.message
print '结束'
多进程
def process_main():
"""多进程,获取前五页数据"""
p = Pool()
offsets = [offset for offset in range(0,100,20)]
p.map_async(main,offsets)
p.close()
p.join()```
完整代码如下:
#coding=utf-8
"""测试模块"""
import os
import re
import json
import imghdr
import StringIO
import hashlib
import pymongo
import requests
import pymongo.errors
from multiprocessing import Pool
from bs4 import BeautifulSoup
import config
client = pymongo.MongoClient(config.MONGO_URL)
db = client[config.MONGO_DB]
db_table = db[config.MONGO_TABLE]
#创建唯一索引 url
db_table.create_index([('url',pymongo.DESCENDING)],unique=True)
def get_page_index(offset=0):
"""获得索引页内容
:return 返回json格式数据"""
url = 'http://www.toutiao.com/search_content/?format=json&keyword=街拍&autoload=true&count=20&cur_tab=3'
headers = {
'Host':"www.toutiao.com",
'Referer':"http://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D"
}
params = {'offset': offset}
html = downloader(url,headers,params)
return html
def get_page_detail(url):
"""获得详情页I新闻页)html源码
:return 详情页html源码"""
print '下载------',url
headers = {
'Host': "www.toutiao.com",
'Referer': "http://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D"
}
html = downloader(url,headers=headers)
return html
def parse_page_index(html):
"""解析索引页内容,获得新闻url
:raise KeyError 当没有数据时候抛出KeyError异常
:return 新闻url"""
data = json.loads(html)
#这个地方要加一个 data['data']
# 因为data['data']为空,表示索引页已经到底,没有下一页了。
# 其实下一页还可以继续访问,只是返回的全部是重复数据。
if data and ('data' in data) and data['data']:
for item in data['data']:
yield item['url']
else:
raise KeyError(u'没有数据了')
def parse_page_detail(html,url):
"""解析详情页,抽取标题,图片url
:return {url,title,urls} 详情页信息 字典集合"""
soup = BeautifulSoup(html,'lxml')
title = soup.title.string
images_pattern = re.compile('var gallery =.*?(.*?);',re.S)
result = re.search(images_pattern, html)
if result:
data = json.loads(result.group(1))
if data and ('sub_images' in data):
sub_images = data['sub_images']
images = [item['url'] for item in sub_images]
# 放到这里回导致重复下载图片,所以 移动到parse_detail中
# 只有插入数据库成功,才会下载图片,避免因重新运行程序导致对已下载过的图片进行重复的下载。
# for image_url in images:
# download_image(image_url)
return {
'url':url,
'title':title,
'images':images
}
def download_image(url):
"""下载图片并保存到当前文件夹"""
image = downloader(url,isBinary=True)
save_image(image)
print '下载图片成功 ---- ',url
def save_image(image):
"""保存图片到当前目录下的images文件夹内"""
image_file = 'images'
image_name = hashlib.md5(image).hexdigest()
# 文件后缀
# imghdr.what(f)返回图片文件的格式
# 只接受文件对象,所以用StringIO包装一下
f = StringIO.StringIO(image)
image_suffix = imghdr.what(f)
try:
if not os.path.exists('images'):
os.mkdir('images')
file_path = '{0}{1}{2}.{3}'.format(image_file, os.path.sep, image_name, image_suffix)
with open(file_path,'wb') as f:
f.write(image)
except IOError as e:
print '文件操作失败 --- ',e
def save_db(result):
"""保存数据到mongodb中
:return 插入数据库成功返回True,否则返回Flase"""
status = False
try:
if db_table.insert(result):
print '保存数据库成功 ',result
status = True
except pymongo.errors.DuplicateKeyError:
# 对唯一字段进行重复插入,pymongo则会抛出这个错误,并且插入失败
print '重复插入'
pass
return status
def downloader(url,headers=None,params=None,again_num=3,isBinary=False):
"""根据url下载html
requests 并不会主动抛出http 状态码异常,
我们只处理服务器异常也就是状态码为500与600之间。服务器异常则重试3次下载
服务器异常的情况下主动抛出requs状态码异常
对超时异常,同样进行重试3次下载
:param again_num 下载异常时,重复下载次数
:param isBinary:是否下载二进制数据,例如图片 True表示下载二进制数据,Flase表示 下载普通html页面
:return 返回html源码页面"""
html = None
try:
response = requests.get(url, headers=headers, params=params)
code = response.status_code
if code == 200:
html = response.content if isBinary else response.text
elif 500 <= code < 600:
response.raise_for_status()
except requests.HTTPError as e :
if again_num > 0:
print ' >>> 服务器异常,下载重试 '
return downloader(url,headers=headers,params=params,again_num=again_num-1)
except requests.Timeout as e:
if again_num > 0:
print ' >>> 超时异常,下载重试 '
return downloader(url,headers=headers,params=params,again_num=again_num-1)
except requests.ConnectionError as e:
print ' >>> 网咯问题,下载失败 --- ',url
return html
def parse_index(html):
"""解析索引页"""
for url in parse_page_index(html):
page_html = get_page_detail(url)
if page_html:
parse_detail(page_html,url)
def parse_detail(html,url):
"""解析详情页"""
result = parse_page_detail(html, url)
stauts = save_db(result)
# 当数据库插入成功的情况下 进行图片下载,避免重复访问导致的重复图片下载
if stauts:
for image_url in result['images']:
download_image(image_url)
def main(offset):
#parse_index 与 parse_detail 是因为main嵌套层次太多(5层)所以拆分的。
try:
html = get_page_index(offset)
if html:
parse_index(html)
except KeyError as e:
print e.message
print '结束'
def process_main():
"""多进程,获取前五页数据"""
p = Pool()
offsets = [offset for offset in range(0,100,20)]
p.map_async(main,offsets)
p.close()
p.join()
if __name__ == '__main__':
process_main()