查看原文
其他

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

林清猫耳 Python数据科学 2019-04-22
点击上方“Python数据科学”,选择“置顶公众号”

关键时刻,第一时间送达!


本文由林清猫耳投稿

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

全文2277字 | 阅读需要12分


前言

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


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


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


所需工具和整个爬虫结构

在写这个爬虫会用到的工具有requestsrejsonpyquery(也可以选择其它的解析工具)。爬虫分为两个部分,第一个部分获取到图片链接,第二个部分将图片保存到本地。这里会接触到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(1800))/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



推荐阅读:


【1】要成为一个专业的爬虫大佬,你还需要了解这些

【2】从爬虫到机器学习预测,我是如何一步一步做到的?

【3】如何用Python过一个完美的七夕节?

【4】还在为找数据而发愁吗?看完这篇你应该再也不会了




长按二维码 关注Python数据科学发送 「学习资料」,获取经典书籍电子书

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存