python 今日头条推荐数据_python 分析Ajax来抓取今日头条街拍美图

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

天善学院 Python爬虫三大案例实战分享

抓取的是街拍中的图集标签下的内容

image.png

流程:

1. 抓取索引页

2. 抓取详情页内容

3. 保存信息到数据库

4. 下载图片

索引页

索引页就是这个页面

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()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值