手把手教你爬取Instagram博主照片和视频

本文由林清猫耳投稿

原文:https://www.jianshu.com/p/b2e077c07c70

全文2277字 | 阅读需要12分

前言

Instagram上有很多非常好看的照片,而且照片类型非常全,照片质量也很高。但是有个问题,不管是在移动端还是在网页端都不能通过长按或者右键方式进行图片保存。

看了下知乎问题 “怎么下载保存 Instagram 上喜欢的图片到手机?” 下的回答,基本都要复制图片链接到其它软件或者微信公众号之类的来获取源图片。于是我就想能不能写一个爬虫,传入一个喜欢的博主账号名称然后爬取该博主所有的照片和视频。

下面是折腾一天后的成果:

所需工具和整个爬虫结构

在写这个爬虫会用到的工具有requestsrejson, pyquery(也可以选择其它的解析工具)。爬虫分为两个部分,第一个部分获取到图片链接,第二个部分将图片保存到本地。这里会接触到javascript动态页面的技术。

获取网页源代码

首先要确保自己对 https://www.instagram.com 发起的请求能返回正常的响应内容。正常的响应内容包括HTML,Json字符串,二进制数据(如图片类型)等类型的内容。

这里不介绍怎么翻墙,能翻墙的小伙伴可以先测试一下,headers请求头要加上user-agentcookie可加可不加,根据自己的情况决定是否要加代理参数proxies,如下图返回的是正常的HTML:

import requests

url = 'https://www.instagram.com/giuliogroebert/'

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
#     'cookie': 'mid=W4VyZwALAAHeINz8GOIBiG_jFK5l; mcd=3; csrftoken=KFLY0ovWwChYoayK3OBZLvSuD1MUL04e; ds_user_id=8492674110; sessionid=IGSCee8a4ca969a6825088e207468e4cd6a8ca3941c48d10d4ac59713f257114e74b%3Acwt7nSRdUWOh00B4kIEo4ZVb4ddaZDgs%3A%7B%22_auth_user_id%22%3A8492674110%2C%22_auth_user_backend%22%3A%22accounts.backends.CaseInsensitiveModelBackend%22%2C%22_auth_user_hash%22%3A%22%22%2C%22_platform%22%3A4%2C%22_token_ver%22%3A2%2C%22_token%22%3A%228492674110%3Avsy7NZ3ZPcKWXfPz356F6eXuSUYAePW8%3Ae8135a385c423477f4cc8642107dec4ecf3211270bb63eec0a99da5b47d7a5b7%22%2C%22last_refreshed%22%3A1535472763.3352122307%7D; csrftoken=KFLY0ovWwChYoayK3OBZLvSuD1MUL04e; rur=FRC; urlgen="{\"103.102.7.202\": 57695}:1furLR:EZ6OcQaIegf5GSdIydkTdaml6QU"'
}

def get_urls(url):
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.text
        else:    
            print('请求错误状态码:', response.status_code)        
    except Exception as e:
        print(e)
        return None
html = get_urls(url)
print(html)

以下是获取的网页源代码:

分析页面

选择一位自己喜欢的博主然后分析Instagram的响应内容HTML。首先检查index页面的HTML文件中是否存在图片链接。

缩略图

可以看到index页面的HTML文件中是有图片链接的,但是复制该图片div的类名v1Nh3 kIKUG _bz0w的字符串去Source Tab页下查找,发现并没有结果,发现里面的内容都是动态生成的。

Source

右键查看网页源代码或者按Ctrl+U,然后Ctrl+F搜索刚看见的图片链接,可以发现网页源代码中有图片链接,不过数据是通过Ajax异步请求过来的。

Find URL

可以发现被script包裹在里面的windows._shareData,图片的链接就在里面,并且数据格式还是 json 格式的。将其单独提取出来放在在线代码格式化工具 format 一下:

json数据块

发现真正的图片链接 display_url 就在该 nodes 数据中。该部分代码实现如下:

import requests

url = 'https://www.instagram.com/giuliogroebert/'

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
#     'cookie': 'mid=W4VyZwALAAHeINz8GOIBiG_jFK5l; mcd=3; csrftoken=KFLY0ovWwChYoayK3OBZLvSuD1MUL04e; ds_user_id=8492674110; sessionid=IGSCee8a4ca969a6825088e207468e4cd6a8ca3941c48d10d4ac59713f257114e74b%3Acwt7nSRdUWOh00B4kIEo4ZVb4ddaZDgs%3A%7B%22_auth_user_id%22%3A8492674110%2C%22_auth_user_backend%22%3A%22accounts.backends.CaseInsensitiveModelBackend%22%2C%22_auth_user_hash%22%3A%22%22%2C%22_platform%22%3A4%2C%22_token_ver%22%3A2%2C%22_token%22%3A%228492674110%3Avsy7NZ3ZPcKWXfPz356F6eXuSUYAePW8%3Ae8135a385c423477f4cc8642107dec4ecf3211270bb63eec0a99da5b47d7a5b7%22%2C%22last_refreshed%22%3A1535472763.3352122307%7D; csrftoken=KFLY0ovWwChYoayK3OBZLvSuD1MUL04e; rur=FRC; urlgen="{\"103.102.7.202\": 57695}:1furLR:EZ6OcQaIegf5GSdIydkTdaml6QU"'
}

def get_urls(url):
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.text
        else:    
            print('请求错误状态码:', response.status_code)        
    except Exception as e:
        print(e)
        return None
html = get_urls(url)
print(html)

import json
from pyquery import PyQuery as pq

urls = []
doc = pq(html)
items = doc('script[type="text/javascript"]').items()

for item in items:
    if item.text().strip().startswith('window._sharedData'):
        js_data = json.loads(item.text()[21:-1], encoding='utf-8')
        edges = js_data["entry_data"]["ProfilePage"][0]["graphql"]["user"]["edge_owner_to_timeline_media"]["edges"]
        for edge in edges:
            url = edge['node']['display_url']
            print(url)
            urls.append(url)

获取的urls如下:

https://instagram.fhkg4-2.fna.fbcdn.net/vp/45abb85604bbbab3404eff130918b341/5C20C452/t51.2885-15/e35/39486322_2187237271565918_3618712295674216448_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/49bd9200153f95247287d9070e9cabd1/5C1C2B43/t51.2885-15/e35/39368892_459599201204846_5293870852465491968_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/7f0a8d4c63e1365d312839324717bd35/5C2BAB3F/t51.2885-15/e35/40405440_1912743685697380_1896252027101511680_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/c9cbe1997f265872f0846e1710abc573/5C1B8F80/t51.2885-15/e35/40213201_677633542617660_3340171924088029184_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/05a4cc0d68d5e807f03c7158e66fb695/5C2F94A8/t51.2885-15/e35/39119198_1987552014870344_2128140489688350720_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/a0069c3ec13a606c271dd520892bcef9/5C39BB8F/t51.2885-15/e35/39104640_2189245444481166_4017361285959122944_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/c14d529d824ed1c0ca5fa38d6d712858/5C252605/t51.2885-15/e35/39333335_696098567422434_857412215450370048_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/a70dffa6f7ecaaec0c84ba347b05d504/5C2CF860/t51.2885-15/e35/39128029_247878022734181_8856032250056146944_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/f6a3ca683561646afa54b3bf47baac14/5C35B54A/t51.2885-15/e35/39110936_1119379814879782_8111482429295820800_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/e78e806bf5420795ac3f8b4f12e672de/5C2F0C3B/t51.2885-15/e35/39244161_703886479992139_1013313030009651200_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/8a3cf794fedc3582a6f955e4c78e8d75/5C219008/t51.2885-15/e35/39399200_582273198853874_593214861379371008_n.jpg
https://instagram.fhkg4-2.fna.fbcdn.net/vp/0be1eee02ae282de1d309b7c36419b41/5C323969/t51.2885-15/e35/39296781_935769909959360_7346812078323138560_n.jpg

到这里确实已经拿到了该Ins博主的照片url,但是这里只有12条,那么其它的照片url在哪里呢?

分析XHR

通过鼠标下拉会不断加载新的图片,这些图片也是通过Ajax异步请求过来的,于是我去查看XHR请求:

XHR

一种开炉石卡包开出橙卡的 "传说!" 的感觉!发现在鼠标下拉页面的时候,会不断加载出新的XHR请求,并且这些XHR请求的响应内容都是Json字符串,于是复制XHR请求的url重复操作一下果然得到了第13张图片开始的url。

urls

这里新的问题出现了,一条XHR请求还是只有12张图片啊,这位博主一共有近500条帖子,仅为了12张图片就要去看XHR请求复制url一次也太反人类了。于是开始分析XHR请求的url。

分析XHR请求的URL

下面是其中一条XHR请求的url:

https://www.instagram.com/graphql/query/?query_hash=a5164aed103f24b03e7b7747a2d94e3c&variables=%7B%22id%22%3A%221664922478%22%2C%22first%22%3A12%2C%22after%22%3A%22AQBJ8AGqCb5c9rO-dl2Z8ojZW12jrFbYZHxJKC1hP-nJKLtedNJ6VHzKAZtAd0oeUfgJqw8DmusHbQTa5DcoqQ5E3urx0BH9NkqZFePTP1Ie7A%22%7D

其中的参数有:

query_hash: a5164aed103f24b03e7b7747a2d94e3c
variables: {
"id":"1664922478",
"first":12,
"after":"AQBJ8AGqCb5c9rO-dl2Z8ojZW12jrFbYZHxJKC1hP-nJKLtedNJ6VHzKAZtAd0oeUfgJqw8DmusHbQTa5DcoqQ5E3urx0BH9NkqZFePTP1Ie7A"}

这里的id应该就是该博主的一个id序列,而这里的first参数则应该是每次XHR请求返回的图片url的数量。于是我在XHR请求的url中将该参数从12改成了24,发现真的返回了24条图片url!

我心想这下问题该解决了吧,只要把first改成图片总数-12不就可以爬取所有图片了。

count

如图,我发现XHR请求的响应内容里直接就有count参数,于是我定位到count将XHR请求的url里的first参数改成count-12,然后开始美滋滋得下载图片。

第一次下载只有62张图片,于是新建一个文件夹重新下载,还是只有62张图片。其中前12张是从HTML文件总取得的,那么后面这50张图片应该就是该XHR请求返回的urls。这下我意识到,一次XHR请求返回的Json字符串最多只能容纳50条图片url,所以这个办法是行不通的。

这时候我注意到url里的after参数,我开始猜测这个参数应该是包含该响应内容一串加密数据。那么我要怎么去找这串加密数据呢,怎么去找每一条XHR请求的url里的after参数的值呢,这串加密数据又具体是什么作用呢?

经过一段 "在哪里,在哪里找到你 的寻寻觅觅" 后,我发现在XHR的响应内容Json字符串不起眼的下面:

page_info

我的内心:"金色传说!" 看参数名end_cursorhas_next_page就大概猜到了这两个参数的作用(所以参数名起名还是很重要滴)。经过一系列在 Jupyter notebook 上的测试发现:

每一条XHR请求的url只有after参数不同,其它三个参数query_hashidfirst都相同。当然不同博主的id肯定不一样,first参数也无关紧要默认的值是12就行游标end_cursor是下一条XHR请求的url里的after参数的值has_next_page是对该url是否是最后一条url的判定布尔值。

也就是说这些看似一团乱码的XHR请求的url其实都是有序的,从包含第13-24张帖子内容的url开始,按博主发帖子的时间顺序构成XHR请求的url序列,每条url的响应内容包含12条图片或视频链接。

所以可以通过一个while循环不断发起XHR请求直到参数has_next_page参数的值为False时退出循环,并在每次的响应内容里提取12张图片的url和参数end_cursorhas_next_page的值。

一些小问题

爬虫到了这里其实已经完成的差不多了,但还是有一些小问题。

问题1:初始游标

现在可以通过XHR请求的响应内容提取下一条XHR请求的url参数值以进行全部图片的url提取。但是每一条XHR请求的url包含的都是下一条XHR请求的url参数值,那么第一条XHR请求的url参数怎么确定?

一种办法是查看博主Ins主页,按F12,选中 Network --> XHR 下拉,手动复制粘贴第一条XHR请求的url中的after参数值。(我一开始也是这么做的) 但是!这样还是太反人类了!一开始的HTML文件中一定有该cursor!嗯,果不其然:

cursor

经过测试后这条end_cursor确实是第一条XHR请求的url参数after的值。将其提取定位并提取传入第一条XHR请求的url中即可解放双手。

问题2:博主id

用中学数学常说一个词:同理可得。 嗯同理可得,博主id在一开始的HTML文件中也一定用,直接用正则匹配一下就有了然后传入每一条XHR请求的url中即可真正实现解放双手。

贴上问题1和问题2部分代码:

urls = []
user_id = re.findall('"profilePage_([0-9]+)"', html, re.S)[0]
print('user_id:' + user_id)
doc = pq(html)
items = doc('script[type="text/javascript"]').items()
for item in items:
    if item.text().strip().startswith('window._sharedData'):
        js_data = json.loads(item.text()[21:-1], encoding='utf-8')
        edges = js_data["entry_data"]["ProfilePage"][0]["graphql"]["user"]["edge_owner_to_timeline_media"]["edges"]
        page_info = js_data["entry_data"]["ProfilePage"][0]["graphql"]["user"]["edge_owner_to_timeline_media"]['page_info']
        cursor = page_info['end_cursor']
        flag = page_info['has_next_page']
        for edge in edges:
            if edge['node']['display_url']:
                display_url = edge['node']['display_url']
                print(display_url)
                urls.append(display_url)
        print(cursor, flag)

第64行和第56行

问题3:视频

到这一步已经实现只传入博主账号名称提取该博主所有图片url的骚操作了。但经过几个博主的爬取实测,发现原本的视频爬下来只是图片,于是继续分析XHR请求的响应内容Json字符串内容。

video

如图,发现每个node都有一个is_video参数,并且另有video_url,于是加一个视频判定并另外提取url即可,代码如下:

while flag:
    url = uri.format(user_id=user_id, cursor=cursor)
    js_data = get_json(url)
    infos = js_data['data']['user']['edge_owner_to_timeline_media']['edges']
    cursor = js_data['data']['user']['edge_owner_to_timeline_media']['page_info']['end_cursor']
    flag = js_data['data']['user']['edge_owner_to_timeline_media']['page_info']['has_next_page']
    for info in infos:
        if info['node']['is_video']:
            video_url = info['node']['video_url']
            if video_url:
                print(video_url)
                urls.append(video_url)
        else:
            if info['node']['display_url']:
                display_url = info['node']['display_url']
                print(display_url)
                urls.append(display_url)
    print(cursor, flag)
    # time.sleep(4 + float(random.randint(1, 800))/200)    # if count > 2000, turn on
return urls

85行 - 89行

爬取效果

爬取效果如图:

get_urls

crawling_start

crwaling_end

部分博主

图片示例

Ps: 图片示例中的图片名称使用了hashlib模块中的md5加密算法,对图片二进制流进行加密使每张图片都有唯一的图片名,以便以后更新博主图片防止重复下载。

该部分代码如下:

file_path = r'C:\Users\Ph\Pictures\Instagram\{0}\{1}.{2}'.format(user, i, urls[i][-3:])
    if not os.path.exists(file_path):
        with open(file_path, 'wb') as f:
            print('正在下载第{i}张:'.format(i=i) + urls[i], '还剩{0}张'.format(len(urls)-i-1))
            f.write(content)

这里的图片路径是我原先创建好的以博主账号名称为名的文件夹,后缀是提display_urlvideo_url的后三位,分别是jpg格式和mp4格式。

最后的小问题

1. 429状态码

若博主帖子数目太多中途请求json的时候会返回一个429的状态码。

响应状态码429 Too Many Requests

经过测试,2000条以内不会返回429,若爬取的博主有2000条以上帖子可以在请求json的时候加一点延迟,如上图代码块中的第96行。

2. 视频文件

由于前12条帖子是在一开始的HTML文件中提取到的,我没有找到包含前12条帖子内容的XHR请求的url,也没有在该HTML文件中找到包含视频内容的url链接。但该链接在网页Elements中是包含在一条a标签的href中。如下图蓝色那条:

video_url

所以,博主前12条帖子里如果有视频则只能拿到一张展示图片。其次,类似的问题还有如果博主发的是超过1张的照片组,也只能拿到其中的第一张照片。

3. 下载方式

这里我选择先将拿到的所有图片或视频url保存在一个列表urls中,再遍历urls下载所有图片或视频。也可以选择每拿到一条照片或视频url就下载到本地。

4. 爬虫效率

这里没有使用爬虫框架,也没有使用多线程。因为该爬虫只是出于学习交流的目的而写。

后记

以上就是所有的Instagram爬虫的爬虫逻辑和部分代码。初学不久,如有相关术语使用错误欢迎评论或私信指正。如有其它错误也欢迎评论或私信指正,如有上述小问题的解决方法或其它问题欢迎私信交流,最后,欢迎评论推荐Ins博主 (๑>◡<๑)

完整代码详见链接: https://github.com/linqingmaoer/Instagram_crawler

原文发布于微信公众号 - Python数据科学(Python_Spiderman)

原文发表时间:2018-08-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张戈的专栏

DX-Seo与WP-codebox插件冲突解决方法

这两天折腾代码高亮插件,折腾得够呛!缘由要不就是导致网站爆卡,要不就是点击复制按钮出问题。 这里说下 wp-codebox 和 DX-Seo 插件冲突的现象及解...

30090
来自专栏菩提树下的杨过

flex中使用swc实现更好的界面代码分离

前几天写过一篇"flash开发中如何实现界面代码分离",评论中 小-G 同学给出了更好的建议:swc ,今天试用了一下,果然比较embed swf来得更爽!同时...

22160
来自专栏Danny的专栏

html页面导出为pdf(jsPDF、iText、wkhtmltopdf)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

1.8K10
来自专栏Java Web

SpringBoot技术栈搭建个人博客【前台开发/项目总结】

先吐槽一句..写页面是真的不擅长,然后限于时间的问题,我开始考虑换用Bootstrap来完成页面的编写,因为时间有限(我得在实习完之前把所有页面开发完),学习V...

31250
来自专栏Material Design组件

Human Interface Guidelines —— Edit Menus

14160
来自专栏数据小魔方

左手用R右手Python系列——任务进度管理

一直觉得运行代码的时候,如果有一个提示任务运行进度的进度条提示就好,很多时候我们的程序运行时间普遍较长,如果程序运行没有任何提示,那简直是一场噩梦,根本不知道到...

41050
来自专栏顶级程序员

高效 MacBook 工作环境配置

工欲善其事,必先利其器,工具永远都是用来解决问题的,没必要为了工具而工具,一切工具都是为了能快速准确的完成工作和学习任务而服务。 本文记录 MacBook 整...

77170
来自专栏Flutter入门到实战

2017年你绝对想尝试的25个新安卓库(持续更新中...)

这是一份2017年1,2月份发布的25个最佳安卓库的列表,你应该会喜欢,虽然是按顺序排列的,但排名不分先后。让我们开始吧!

27720
来自专栏琯琯博客

超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集

一、谷歌浏览器插件 二、Sublime Text 插件 三、Phpstorm 插件 四、油猴脚本 4.1 脚本网站 4.2 自用的脚本 五、相关链接 ? 分享...

1.3K80
来自专栏FreeBuf

Google最新XSS Game Writeup

本文介绍了如何完成谷歌最新的XSSGame的过程,完成了这八个挑战就有机会获得Nexus 5x。实际上这八个挑战总体来说都不难,都是些常见的xss。通关要求是只...

258100

扫码关注云+社区

领取腾讯云代金券