20119.3.25更新:今日头条的“图集”模块已经改为“视频”了,可能是被人爬多了?
———————————分割线——————————
有一些网页直接请求得到的HTML代码并没有在网页中看到的内容,因为一些信息是通过Ajax加载,并通过js渲染生成的,这时就需要通过分析网页的请求来获取想要爬取的内容。本文通过抓取今日头条街拍美图讲解一下具体操作步骤。
网络库:Requests
解析库:BeautifulSoup+正则表达式
存储数据库:MongoDB
其他库:PyMongo
请确保以上库已经正确安装。
打开今日头条的网页并搜索“街拍”。
我们想要抓取的是这些图集里面的内容。
右击空白处->审查->Network->勾选Preserve log->刷新网页
查看URL返回的都是一些js,并没有我们想要获取的内容。
点击XHR,再选中一个URL,查看请求的方法,发现是用get方法,所以使用requests库。
再看Preview,里面有许多data。我们展开来核对一下。
核对一下第一个data的title是不是和当前页面的第一个标题相同呢?如果是,那么说明当前网页的展示和对应的代码是没有问题的。并且URL的对应也没有问题。
现在下拉页面,可以看到左下方不断出现了新的URL请求:
重点在于都是通过offset这个参数的改变来实现的——变化范围为0,20,40···!那么我们通过循环就可以拿到这个“街拍”下组图形式的所有数据了!通过右边的数据可以看到这些都是一些json的数据,我们拿到后台数据后只需要调用json的包进行解析就可以了。
接下来就是分析查找图集详细页的代码,来找到图片的url,这个图片url隐藏的比较深,都在JS代码中:
通过比对发现确实如此。
由于这个url是藏在gallery这个变量里的,然而这个变量并不是在html代码里的,所以不能使用BeautifulSoup和PyQuery来解析了,只能通过正则表达式来解析。
1.获取索引页内容
利用requests请求目标站点,得到索引网页HTML代码,返回结果。
2.抓取详情页信息
解析返回结果,得到详情页的链接,并进一步抓取详情页的信息。
3.下载图片与保存数据库
将图片下载到本地,并把页面信息及图片URL保存至MongoDB。
4.开启循环及多线程
对多页内容遍历,开启多线程提高抓取速度。
再看一下索引页的请求方式:
我们只需要按照这个格式构建一个Ajax请求。
注意!cur_tab为3时,搜索到的才是图集(一共有4个标签:综合、视频、图集、用户)。
from urllib.parse import urlencode
import requests
from requests.exceptions import RequestException
headers = {'User-Agent':'Mozilla/5.0(Macintosh;Intel Mac OS X 10_11_4)AppleWebKit/537.36(KHTML,like Gecko)Chrome/52.0.2743.116 Safari/537.36'}#这个信息是自定义的,根据自身需求来改变
#请求索引页(索引页中包含着许多图集的url)
def get_page_index():
data = {#定义一个data字典,用于Ajax请求
'offset': 0,
'format': 'json',
'keyword': '街拍',
'autoload': 'true',
'count': '20',
'cur_tab': '3',
'from': 'gallery'#这一行一定不能少
}
url = 'https://www.toutiao.com/search_content/?'+urlencode(data)
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
print("请求索引页错误")
return None
def main():
html=get_page_index()
print(html)
if __name__ == '__main__':
main()
注意!由于不同浏览器请求时的headers信息是不同的,所以在定义headers时可以到你的常用浏览器中去获取,随意打开一个网页右键审查,点击一个元素network然后查看Headers:
可以把下图红框中的信息复制到代码中作为headers(注意格式是个字典)。
运行一下:
如上,成功获得了索引页的html,里面包含着许多图集的url。
由于上文返回的html是json格式的字符串对象——我们调用type方法就可以看出来:
print(type(html))
因此我们需要调用json.loads()方法对字符串进行解析。
json.loads()用于将str类型的数据转成字典。
再仔细分析下图,这是索引页的html(json格式)。可以看到,data对应还有许多值。
将这些值(0,1,2…)展开,可以看到每一个值又是一个字典(abstract,article_url…),我们要提取的就是这些子层字典中“article_url”对应的值。
#注意引入相关的包
import json
from json.decoder import JSONDecodeError
#传入索引页的html,解析出每个图集的url
def parse_page_index(html):
try:#加入异常处理
data = json.loads(html)#对html进行解析,转换为字典。
if data and 'data' in data.keys():
#data.keys()返回的是这个字典中的所有的键名,并判断:'data'这个键名是否其中,若在的话执行下面的for循环
for item in data.get('data'):#data这个键对应着许多值,遍历这些值,并依次赋值给item
yield item.get('article_url')#构造一个生成器,取出每一个item中的article_url对应的url
except JSONDecodeError:#如果出现了JSON解析异常,则跳过
pass
然后在main函数中调用以上的函数,解析出图集的url,这些url就是每个图集的入口。
def main():
html=get_page_index()
for url in parse_page_index(html):#通过生成器提取所有的url
print(url)
若尝试进入上面提取到的url,那么则会进入详情页(也就是进入了某个图集)。现在我们要获取详情页的代码(因为我们最终要抓取的图片就隐藏在这些代码之中)。这部分很好理解,和1.获取索引页的代码是相同的。
#请求每个图集的详情页
def get_page_detail(url):
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
print('请求详情页出错',url)
return None
再次在main函数中改动一下,把上面获取到的详情页的text打印一下,以此来检查到此为止一切是否顺利:
def main():
html=get_page_index()
for url in parse_page_index(html):#通过生成器提取所有的url
print(get_page_detail(url))#依次请求(上面提取到的url)并打印返回的text
好,成功请求了url,并且得到了它们的text。通过type()可以知道,这些text都是str类型的。
关键的一步到了。我们先研究一下详情页的源代码。
要在“Doc”中才能看到比较原始的代码。我们再找找图片的url隐藏在哪里。
发现图片的url都在gallery键的值中(并且值里面还有许多的“\”符号)
现在定义一个函数来解析详情页的数据,目标是把这个图集下的所有图片url提取出来。
首先,获取每个图片集的标题title,用BeautifulSoup下的select方法选择title标签下的文本。(因为文本直接能用,所以这时候就可以使用特别方便的BeautifulSoup了,而下面的图片url就不是这样,还掺杂着别的信息)
from bs4 import BeautifulSoup#记得导入
#解析详情页,获取图集中每张图片的url
def parse_page_detail(html):
soup = BeautifulSoup(html,'lxml')#传入解析器:lxml和解析对象:html
title = soup.select('title')[0].get_text()
#因为select返回的结果是一个list,所以要用[0]来指定元素(也就是第一个元素),这个元素的类型是bs4.element.Tag
#get_text()方法是定义在bs4.element.Tag这个类上面的,而不是list上
#get_text()方法获取“title”对应的内容
print(title)
在main函数中判断html是否正确,并打印结果:
main():
html = get_page_index()
for url in parse_page_index(html):
html = get_page_detail(url)
if html:
parse_page_detail(html)
接下来获取每个图片集中的图片信息,所有图片信息都在gallery键的值中,通过re.comlile构建一个正则表达式pattern,再search得到结果,因为此时得到的结果中信息不正确,有很多多余的反斜杠’\’,于是利用replace去掉斜杠。
这一步的关键是正则表达式的写法。
注意,由于不同的浏览器返回的代码有可能不同,所以根据自己在浏览器(这个浏览器的headers应该与代码中的相对应,否则可能出错)中看到的代码来写正则表达式。
我们要匹配的是上图蓝色框中的内容(夹在括号内)。
import re
#下面提取json串,串中包含了图片信息
images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)#注意对括号进行转义
result=re.search(images_pattern,html)
if result:
result = result.group(1).replace('\\', '')#替换反斜杠为空格
结果是json字符串的格式,需要用loads解析,提取其中的每张照片的url,最后返回的是图集的标题、链接和每张图片的url。
以上一步的思路进一步完善:
def parse_page_detail(html,url):#多传入一个当前详情页的url参数
soup = BeautifulSoup(html,'lxml')#传入解析器:lxml和解析对象:html
title = soup.select('title')[0].get_text()
#get_text()方法获取“title”对应的内容
print(title)
# 下面提取json串,串中包含了图片信息
images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S) # 注意对括号进行转义
result = re.search(images_pattern, html)
if result:
result = result.group(1).replace('\\', '')
data = json.loads(result) # 转换成json对象
if data and 'sub_images' in data.keys():
sub_images = data.get('sub_images')
# 每个sub_images都是一个字典,需要遍历它来提取url元素
# 用一句话来构造一个list,把item赋值为sub_images的每一个子元素
# 再取得sub_images的每一个item对象的url属性,完成列表的构建,这个列表名为images,里面是sub_images下所有的url
images = [item.get('url') for item in sub_images]
return { # 以一个字典形式返回
'title': title,
'url': url, # 这是当前详情页的url
'images': images
}
此时,所有的信息已经提取完毕,开始存储数据到MongoDB数据库。
要把数据存储到mongodb数据库中,首先在同一目录下,建立配置文件config.py。
这个配置文件需要写入以下内容:
MONGO_URL='localhost' #链接地址
MONGO_DB='toutiao' #数据库
MONGO_TABLE='toutiao' #数据集即“表”
通过from config import *调用该文件:
from config import *
import pymongo
#声明mongodb数据库对象
client=pymongo.MongoClient(MONGO_URL)
db=client[MONGO_DB]
然后定义函数存储到数据库中,并判断如果存储成功输出相应信息
#把url存储到数据库
def save_to_mongo(result):
if db[MONGO_TABLE].insert(result):
print('存储到MongoDB成功',result)
return True
return False
这个函数将在主函数中调用:
def main(offset):
html=get_page_index(offset, KEYWORD)
for url in parse_page_index(html):#获得每个图集的url
html=get_page_detail(url)#用某个图集的url来请求详情页
if html:
result=parse_page_detail(html,url)#解析详情页的信息
if result:save_to_mongo(result)#保存到数据库
首先定义一个函数,利用pathlib库,根据传入的目录名创建一个文件目录,这是为了将图片分类:
from pathlib import Path
def create_dir(name):
#根据传入的目录名创建一个目录,这里用到了 python3.4 引入的 pathlib 。
directory = Path(name)
if not directory.exists():
directory.mkdir()
return directory
然后定义下载图片函数,要求返回的是content,是二进制文件:
def download_image(save_dir,url):
print("正在下载:",url)
try:
response = requests.get(url,headers=headers)#还是熟悉的请求方式
if response.status_code == 200:
#调用存储图片函数,返回二进制
save_image(save_dir,response.content)#调用存储图片的函数
return None
except RequestException:
print("请求图片出错",url)
return None
定义存储图片函数
import os
from hashlib import md5
def save_image(save_dir,content):
'''把文件保存到本地,文件有三部分内容(路径)/(文件名).(后缀)
用format构造字符串(项目路径,文件名,格式),md5文件名可以避免重复'''
#os.getcwd()程序同目录,但是现在我们要自定义目录
#file_path='{0}/{1}.{2}'.format(os.getcwd(),md5(content).hexdigest(),'jpg')
file_path = '{0}/{1}.{2}'.format(save_dir, md5(content).hexdigest(), 'jpg')
#如果文件不存在,开始存入
if not os.path.exists(file_path):
with open(file_path,'wb') as f:
f.write(content)
f.close()
在parse_page_detail函数中,调用download_image:
root_dir=create_dir('E:\spider\\'+KEYWORD) # 保存图片的根目录,这个是自定义的,E:\spider这个文件夹需要提前在本地建好。此后程序会根据KEYWORD建一个子文件夹。create_dir函数是上面我们定义过的。
download_dir = create_dir(root_dir / title) # 根据每组图片的title标题名创建目录
for image in images:
download_image(download_dir, image) #下载所有的图片
为了方便代码的复用,还可以把offset、搜索关键词等参数放到配置文件中:
MONGO_URL='localhost' #链接地址
MONGO_DB='toutiao' #数据库
MONGO_TABLE='toutiao' #数据集即表
GROUP_START = 1
GROUP_END = 20
KEYWORD = '街拍'#若想爬取其他内容,在此替换关键词即可
开启多线程可以提高抓取效率:同时下载多个页面的图片
循环可以抓取更多页面的信息
from multiprocessing import Pool
if __name__ == '__main__':
groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]
#把offset做成一个列表20,40,60...
#GROUP_START,GROUP_END用来限制起始和结束时的offset,也就是想要爬取的页面范围,这已在配置文件中定义过了
pool=Pool()
pool.map(main,groups)#将列表传入主函数,并且开启多线程
还需要修改:
将请求索引页时的offset和keyword改为由调用方(主函数)传入
修改主函数:
offset是由上面的groups列表传入, KEYWORD是在配置文件中定义的。
def main(offset):
html=get_page_index(offset, KEYWORD)
import requests
from urllib.parse import urlencode
from requests.exceptions import RequestException
import json
from bs4 import BeautifulSoup
import re
from config import *
import pymongo
import os
from hashlib import md5
from multiprocessing import Pool
from json.decoder import JSONDecodeError
from pathlib import Path
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'}
#声明mongodb数据库对象
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]
#请求索引页(索引页中包含着许多图集的url)
def get_page_index(offset,keyword):
data = {#定义一个data字典,用于Ajax请求
'offset': offset,
'format': 'json',
'keyword': keyword,
'autoload': 'true',
'count': '20',
'cur_tab': '3',
'from': 'gallery'
}
url='http://www.toutiao.com/search_content/?'+urlencode(data)
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
print('请求索引页出错')
return None
#传入索引页的html,解析出每个图集的url
def parse_page_index(html):
try:#加入异常处理
data = json.loads(html)#对html进行解析,转换为字典。
if data and 'data' in data.keys():#data.keys()返回的是这个json的所有的键名,这里判断'data'在这些键名中
for item in data.get('data'):#data对应还有许多值,遍历这些值
yield item.get('article_url')#构造一个生成器,取出data中的每一个article_url对应的url
except JSONDecodeError:
pass
#请求每个图集的详情页
def get_page_detail(url):
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
print('请求详情页出错',url)
return None
#解析详情页,获取图集中每张图片的url
def parse_page_detail(html,url):
soup = BeautifulSoup(html, 'lxml')
# 用BeautifulSoup来提取title信息
title = soup.select('title')[0].get_text()
print(title)
#下面提取json串,串中包含了图片信息
images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)#注意对括号进行转义
result=re.search(images_pattern,html)
if result:
result = result.group(1).replace('\\', '')
data = json.loads(result)#转换成json对象
if data and 'sub_images' in data.keys():
sub_images = data.get('sub_images')
#每个sub_images都是一个字典,需要遍历它来提取url元素
# 用一句话来构造一个list,把item赋值为sub_images的每一个子元素
# 再取得sub_images的每一个item对象的url属性,完成列表的构建,这个列表名为images,里面是sub_images下所有的url
images = [item.get('url') for item in sub_images]
root_dir=create_dir('E:\spider\jiepai')
download_dir = create_dir(root_dir/title)
for image in images: download_image(download_dir,image)#通过循环把图片下载下来
return {#以一个字典形式返回
'title':title,
'url':url,#这是当前详情页的url
'images':images
}
#把url存储到数据库
def save_to_mongo(result):
if db[MONGO_TABLE].insert(result):
print('存储到MongoDB成功',result)
return True
return False
#通过url来请求图片
def download_image(save_dir,url):
print('正在下载',url)
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
save_image(save_dir,response.content)#content返回的是二进制内容,一般处理图片都用二进制流
return response.text
return None
except RequestException:
print('请求图片出错',url)
return None
def create_dir(name):
#根据传入的目录名创建一个目录,这里用到了 python3.4 引入的 pathlib 。
directory = Path(name)
if not directory.exists():
directory.mkdir()
return directory
def save_image(save_dir,content):
file_path = '{0}/{1}.{2}'.format(save_dir,md5(content).hexdigest(),'jpg')
if not os.path.exists(file_path):#如果文件不存在
with open(file_path,'wb') as f :
f.write(content)
f.close()
def main(offset):
html=get_page_index(offset, KEYWORD)
for url in parse_page_index(html):#获得每个图集的url
html=get_page_detail(url)#用某个图集的url来请求详情页
if html:
result=parse_page_detail(html,url)#解析详情页的信息
if result:save_to_mongo(result)
if __name__ == '__main__':
groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]#20,40,60...
pool=Pool()
pool.map(main,groups)
最后结果了解一下:
上图是保存在本地目录下的按标题分类好的图片。
上图是使用Studio 3T所查看到的、保存在MongoDB数据库中的信息。
这一次的实战需要掌握以下知识:
总的来说,对一个网页的结构进行正确地分析,确定好提取信息的方案(例如由索引到详情页的请求方法、根据相应网页代码选择正确的库、正则表达式的写法等等),是成功完成类似抓取任务的关键。
ps:直接换个关键词,就可以抓取到别的图片啦!