python 分析Ajax来抓取今日头条街拍美图

本文是学习 天善学院 Python3爬虫三大案例实战分享 / 分析Ajax抓取今日头条街拍美图 后所写,感谢崔庆才崔老师。
  天善学院 Python爬虫三大案例实战分享
抓取的是街拍中的图集标签下的内容

image.png

流程:
 1. 抓取索引页
 2. 抓取详情页内容
 3. 保存信息到数据库
 4. 下载图片

  1. 索引页
      索引页就是这个页面


    image.png

      这个页面就是用ajax来生成的。往下拉动页面,达到边界的时候,Ajax就会自动加载下一页。


    image.png

      鼠标选定的那个就是其中一页索引页,一共加载了四页。
      我们可以看到 四个索引页之间的区别只有 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()

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容