本文目标

目标地址:https://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D

今日头条街拍

爬取今日头条街拍的图片,将图片保存到本地,将标题,URL和图片地址保存到MongoDB数据库中。

爬取结果

今日头条街拍爬取结果

完整代码:https://github.com/huanyouchen/python-spider


过程分析

获取页面

对页面进行请求,然后往下拉页面,会不断的加载进来新的内容,打开Chrome开发者工具分析请求过程:
今日头条街拍请求分析

每次下拉获取新内容,同时查看XHR面板发现加载进来一个地址:
https://www.toutiao.com/search_content/?offset=0&format=json&keyword=%E8%A1%97%E6%8B%8D&autoload=true&count=20&cur_tab=1&from=search_tab
而且offset参数的值每加载一次都会递增20。

查看Headers的Request URL会发现,每次加载请求的URL不是浏览器地址栏的网址:
https://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D, 而是:
https://www.toutiao.com/search_content/?offset=0&format=json&keyword=%E8%A1%97%E6%8B%8D&autoload=true&count=20&cur_tab=1&from=search_tab

所以,爬虫请求的地址应该是在XHR面板不断加载进来的Request URL,同时这个url必须有Query string Parameters下面的几个参数。

这部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KEYWORD = '街拍'
def get_page_first(offset, keyword):
data = {
'offset': offset,
'format': 'json',
'keyword': keyword,
'autoload': 'true',
'count': '20',
'cur_tab': 1,
'from': 'search_tab'
}
url = 'https://www.toutiao.com/search_content/?' + urlencode(data)
try:
res = requests.get(url, headers=headers)
if res.status_code == 200:
return res.text
else:
return None
except RequestException:
print('获取首页请求异常')
return None

每次获取新的内容通过offset参数每次增加20来控制。


获取页面之后,需要解析出页面中每个标题的URL

获取页面后得到了返回的页面内容,查看Preview面板,可以知道data下面有0-19个数据,页面的URL在每个数据下面的article_url中,不过部分数据下面没有article_url,需要过滤掉,代码如下:

1
2
3
4
5
6
def parse_page_first(html):
data = json.loads(html)
if data and 'data' in data.keys():
for item in data.get('data'):
# 构造一个生成器,把所有的article_url解析出来
yield item.get('article_url')

获取标题地址后,需要解析该标题页面的详细内容

在浏览器中请求街拍页面时候,会出现三种形式的内容,第一种是图集;第二种是文章+图片;第三种是视频。请求他们的地址后返回内容如下:

图集形式:
今日头条街拍图集

查看Response面板,这种图集形式图片的URL在gallery: JSON.parse("{\"count\":7,\"sub_images\":里面,
首先用正则匹配到gallery: JSON.parse里面的内容,
然后发现里面的字符串是这样的:
\"url\":\"http:\\/\\/p3.pstatp.com\\/origin\\/pgc-image\\/15285871625860b320c84de\"
需要把\替换为", 把\\替换为''
把格式整理好后,用json.loads转换为json格式内容,
最后提取出sub_images里面的url地址

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 图集形式
images_pattern_gallery = re.compile('gallery: JSON.parse\("(.*?)"\)', re.S)
result_gallery = re.search(images_pattern_gallery, page_res.text)
if result_gallery:
result_gallery = result_gallery.group(1).replace(r'\"', r'"').replace('\\', '')
data = json.loads(result_gallery)
if data and 'sub_images' in data.keys():
sub_images = data.get('sub_images')
imgs_url = [item.get('url') for item in sub_images]
print("准备下载: " + title + " 中的图片" + ", 地址为:" + page_url)
for img_url in imgs_url:
download_img(img_url, title)
return {
'title': title,
'url': page_url,
'images': imgs_url,
}

文章+图片形式:
今日头条街拍文章+图片

查看Response面板,这种图集形式图片的URL在这里面:

1
2
3
articleInfo: {
title: '街拍:北京街拍的美女们',
content: '<p>有喜欢的就点个赞哟<br><br><img src="http://p1.pstatp.com/large/e950002a312048b6134"

同样,用正则匹配到图片的URL地址,代码:

1
2
3
4
5
6
7
8
9
10
11
12
# 文章+图片形式
images_pattern_article = re.compile('http://p(.*?)&', re.S)
result_article = re.findall(images_pattern_article, page_res.text)
if result_article:
article_img_urls = ['http://p' + i for i in result_article]
for article_img_url in article_img_urls:
download_img(article_img_url, title)
return {
'title': title,
'url': page_url,
'images': article_img_urls,
}

视频形式:
今日头条街拍视频

我们只获取图片,需要过滤掉视频。
多看几个视频网页的Response就会发现,视频形式都会有:chineseTag:'视频', 因此:

1
2
3
4
5
6
tag_pattern = re.compile("chineseTag: '(.*?)'", re.S)
tag = re.findall(tag_pattern, page_res.text)[0]
if tag == '视频':
pass
else:
# 获取图集形式和文章+图片形式的内容

解析该标题页面的详细内容得到图片URL后,下载到本地

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def download_img(img_url, title):
try:
img_res = requests.get(img_url, headers=headers)
if img_res.status_code == 200:
if not os.path.exists('img'):
os.mkdir('img')
pathname = 'img/'+title
if not os.path.exists(pathname):
os.mkdir(pathname)
print("正在下载: " + img_url)
filename = pathname + '/' + md5(img_res.content).hexdigest() + '.jpg'
if not os.path.exists(filename):
urllib.request.urlretrieve(img_url, filename=filename)
except RequestException:
print("请求图片出错:" + img_url)
return None

md5(img_res.content).hexdigest(), 这个的作用为每个图片生成唯一的文件名,每次下载图片前先看下是否已经下载过该图片了,如果下载过,就跳过,没有则下载。


将街拍信息保存到MongoDB中

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 设置MongoDB数据库
MONGO_URL = 'localhost'
MONGO_DB = 'jinritoutiao_jiepai'
MONGO_TABLE = 'jiepai_images'
# 链接
client = pymongo.MongoClient(MONGO_URL, connect=False)
db = client[MONGO_DB]
def save_to_mongo(page_detail):
if db[MONGO_TABLE].insert(page_detail):
print("存储到MongoDB成功")
return True
return False
page_detail = get_page_detail(page_url)
if page_detail:
save_to_mongo(page_detail)

将page_detail函数返回的title、url、images内容插入到MongoDB中。