python instagram 爬虫

437 阅读6分钟

直接介绍一下具体的步骤以及注意点:

instagram 爬虫注意点
  • instagram 的首页数据是 服务端渲染的,所以首页出现的 11 或 12 条数据是以 html 中的一个 json 结构存在的(additionalData), 之后的帖子加载才是走 ajax 请求的

  • 在 2019/06 之前,ins 是有反爬机制的,请求时需要在请求头加了 'X-Instagram-GIS' 字段。其算法是:
    1、将 rhx_gis 和 queryVariables 进行组合

    rhx_gis 可以在首页处的 sharedData 这个 json 结构中获得

    2、然后进行 md5 哈希
    e.g.

    queryVariables
    =
    '{"id":"'
    +
    user_id
    +
    '","first":12,"after":"'
    +
    cursor
    +
    '"}'
    print
    (
    queryVariables
    )
    headers
    [
    'X-Instagram-GIS'
    =
    hashStr
    (
    GIS_rhx_gis
    +
    ":"
    +
    queryVariables
    )
  • 但是在在 2019/06 之后, instagram 已经取消了 X-Instagram-GIS 的校验,所以无需再生成 X-Instagram-GIS,上一点内容可以当做历史来了解了

  • 初始访问 ins 首页的时候会设置一些 cookie,设置的内容 (response header) 如下:

    set
    -
    cookie
    :
    rur
    =
    PRN
    ;
    Domain
    =
    .
    instagram
    .
    com
    ;
    HttpOnly
    ;
    Path
    =
    /
    ;
    Secure
    set
    -
    cookie
    :
    ds_user_id
    =
    11859524403
    ;
    Domain
    =
    .
    instagram
    .
    com
    ;
    expires
    =
    Mon
    ,
    15
    -
    Jul
    -
    2019
    09
    :
    22
    :
    48
    GMT
    ;
    Max
    -
    Age
    =
    7776000
    ;
    Path
    =
    /
    ;
    Secure
    set
    -
    cookie
    :
    urlgen
    =
    "{\"45.63.123.251\": 20473}:1hGKIi:7bh3mEau4gMVhrzWRTvtjs9hJ2Q"
    ;
    Domain
    =
    .
    instagram
    .
    com
    ;
    HttpOnly
    ;
    Path
    =
    /
    ;
    Secure
    set
    -
    cookie
    :
    csrftoken
    =
    Or4nQ1T3xidf6CYyTE7vueF46B73JmAd
    ;
    Domain
    =
    .
    instagram
    .
    com
    ;
    expires
    =
    Tue
    ,
    14
    -
    Apr
    -
    2020
    09
    :
    22
    :
    48
    GMT
    ;
    Max
    -
    Age
    =
    31449600
    ;
    Path
    =
    /
    ;
    Secure
  • 关于 query_hash,一般这个哈希值不用怎么管,可以直接写死

  • 特别注意:在每次请求时务必带上自定义的 header,且 header 里面要有 user-agent,这样子才能使用 rhx_gis 来进行签名访问并且获取到数据。切记!是每次访问!例如:

    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'
    }
  • 大部分 api 的访问需要在请求头的 cookie 中携带 session-id 才能得到数据,一个正常的请求头 (request header) 如下:

    :
    authority
    :
    www
    .
    aiidol
    .
    com
    :
    method
    :
    GET
    :
    path
    :
    /
    graphql
    /
    query
    /
    ?query_hash
    =
    ae21d996d1918b725a934c0ed7f59a74
    &
    variables
    =
    %
    7B
    %
    22fetch_media_count
    %
    22
    %
    3A0
    %
    2C
    %
    22fetch_suggested_count
    %
    22
    %
    3A30
    %
    2C
    %
    22ignore_cache
    %
    22
    %
    3Atrue
    %
    2C
    %
    22filter_followed_friends
    %
    22
    %
    3Atrue
    %
    2C
    %
    22seen_ids
    %
    22
    %
    3A
    %
    5B
    %
    5D
    %
    2C
    %
    22include_reel
    %
    22
    %
    3Atrue
    %
    7D
    :
    scheme
    :
    https accept
    :
    *
    /
    *
    accept
    -
    encoding
    :
    gzip
    ,
    deflate
    ,
    br accept
    -
    language
    :
    zh
    -
    CN
    ,
    zh
    ;
    q
    =
    0.9
    ,
    en
    ;
    q
    =
    0.8
    ,
    la
    ;
    q
    =
    0.7
    cache
    -
    control
    :
    no
    -
    cache cookie
    :
    mid
    =
    XI
    -
    joQAEAAHpP4H2WkiI0kcY3sxg
    ;
    csrftoken
    =
    Or4nQ1T3xidf6CYyTE7vueF46B73JmAd
    ;
    ds_user_id
    =
    11859524403
    ;
    sessionid
    =
    11859524403
    %
    3Al965tcIRCjXmVp
    %
    3A25
    ;
    rur
    =
    PRN
    ;
    urlgen
    =
    "{\"45.63.123.251\": 20473}:1hGKIj:JvyKtYz_nHgBsLZnKrbSq0FEfeg"
    pragma
    :
    no
    -
    cache referer
    :
    https
    :
    //
    www
    .
    instagram
    .
    com
    /
    user
    -
    agent
    :
    Mozilla
    /
    5.0
    (
    Macintosh
    ;
    Intel Mac OS X 10_14_3
    )
    AppleWebKit
    /
    537.36
    (
    KHTML
    ,
    like Gecko
    )
    Chrome
    /
    73.0
    .3683
    .103
    Safari
    /
    537.36
    x
    -
    ig
    -
    app
    -
    id
    :
    936619743392459
    x
    -
    instagram
    -
    gis
    :
    8f382d24b07524ad90b4f5ed5d6fccdb
    x
    -
    requested
    -
    with
    :
    XMLHttpRequest

    注意 user-agent、x-ig-app-id (html 中的 sharedData 中获取)、x-instagram-gis,以及 cookie 中的 session-id 配置

  • api 的分页 (请求下一页数据),如用户帖子列表
    ins 中一个带分页的 ajax 请求,一般请求参数会类似下面:

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

    -- id 表示用户 id,可在 html 中的 sharedData 中获取
    -- first 表示初始时获取多少条记录,好像最多是 50
    -- after 表示分页游标,记录了分页获取的位置

    当然 variables 部分里面的参数根据请求的 api 不同而可能不同 (不止这么少),这里只列出与分页相关的参数。

    分页请求参数首先是从 html 中的 sharedData 中获取的:

    # 网页页面信息
    page_info
    =
    js_data
    [
    "entry_data"
    [
    "ProfilePage"
    [
    0
    [
    "graphql"
    [
    "user"
    [
    "edge_owner_to_timeline_media"
    [
    'page_info'
    # 下一页的索引值AQCSnXw1JsoV6LPOD2Of6qQUY7HWyXRc_CBSMWB6WvKlseC-7ibKho3Em0PEG7_EP8vwoXw5zwzsAv_mNMR8yX2uGFZ5j6YXdyoFfdbHc6942w
    cursor
    =
    page_info
    [
    'end_cursor'
    # 是否有下一页
    flag
    =
    page_info
    [
    'has_next_page'

    end_cursor 即为 after 的值,has_next_page 检测是否有下一页
    如果是有下一页,可进行第一次分页数据请求,第一次分页请求的响应数据回来之后,id,first 的值不用变,after 的值变为响应数据中 page_info 中 end_cursor 的值,再构造 variables,连同 query_hash 发起再下一页的请求
    再判断响应数据中的 page_info 中 has_next_page 的值,循环下去,可拿完全部数据。若不想拿完,可利用响应数据中的 edge_owner_to_timeline_media 中的 count 值来做判断,该值表示用户总共有多少媒体

  • 视频帖子和图片帖子数据结构不一样,注意判断响应数据中的 is_video 字段

  • 如果是用一个 ins 账号去采集的话,只要请求头的 cookie 中带上合法且未过期的 session_id,可直接访问接口,无需计算签名。
    最直接的做法是:打开浏览器,登录 instagram 后,F12 查看 xhr 请求,将 request header 中的 cookie 复制过来使用即可,向下面:

    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=XLaW9QAEAAH0WaPDCeY490qeeNlA; csrftoken=IgcP8rj0Ish5e9uHNXhVEsTId22tw8VE; ds_user_id=11859524403; sessionid=11859524403%3A74mdddCfCqXS7I%3A15; rur=PRN; urlgen="{\"45.63.123.251\": 20473}:1hGxr6:Phc4hR68jNts4Ig9FbrZRglG4YA"'
    }

    在请求发出的时候带上类似上面的请求头

  • 错误日志记录表在 192.168.1.57 中 zk_flock 库的 ins_error_log,目前比较多 unknow ssl protocol 类型的错误,怀疑是爬取太快的原因,需要一个代理来切换


给出能运行的代码?(设置了 FQ 代理,不需要的可以去掉喔):

# -*- coding:utf-8 -*-
import
requests
import
re
import
json
import
urllib
.
parse
import
hashlib
import
sysUSER_AGENT
=
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
BASE_URL
=
'https://www.instagram.com'
ACCOUNT_MEDIAS
=
"http://www.smpeizi.com/graphql/query/?query_hash=42323d64886122307be10013ad2dcc44&variables=%s"
ACCOUNT_PAGE
=
'https://www.pzzs168.com/%s'
proxies
=
{
'http'
:
'http://127.0.0.1:1087'
,
'https'
:
'http://127.0.0.1:1087'
,
}
# 一次设置proxy的办法,将它设置在一次session会话中,这样就不用每次都在调用requests的时候指定proxies参数了
# s = requests.session()
# s.proxies = {'http': '121.193.143.249:80'}
def
get_shared_data
(
html
=
''
)
:
"""get window._sharedData from page,return the dict loaded by window._sharedData str """
if
html
:
target_text
=
html
else
:
header
=
generate_header
(
)
response
=
requests
.
get
(
BASE_URL
,
proxies
=
proxies
,
headers
=
header
)
target_text
=
response
.
text regx
=
r"\s*.*\s*<script.*?>.*_sharedData\s*=\s*(.*?);<\/script>"
match_result
=
re
.
match
(
regx
,
target_text
,
re
.
S
)
data
=
json
.
loads
(
match_result
.
group
(
1
)
)
return
data
# def get_rhx_gis():
# """get the rhx_gis value from sharedData
# """
# share_data = get_shared_data()
# return share_data['rhx_gis']
def
get_account
(
user_name
)
:
"""get the account info by username :param user_name: :return: """
url
=
get_account_link
(
user_name
)
header
=
generate_header
(
)
response
=
requests
.
get
(
url
,
headers
=
header
,
proxies
=
proxies
)
data
=
get_shared_data
(
response
.
text
)
account
=
resolve_account_data
(
data
)
return
account
def
get_media_by_user_id
(
user_id
,
count
=
50
,
max_id
=
''
)
:
"""get media info by user id :param id: :param count: :param max_id: :return: """
index
=
0
medias
=
[
has_next_page
=
True
while
index
<=
count
and
has_next_page
:
varibles
=
json
.
dumps
(
{
'id'
:
str
(
user_id
)
,
'first'
:
count
,
'after'
:
str
(
max_id
)
}
,
separators
=
(
','
,
':'
)
)
# 不指定separators的话key:value的:后会默认有空格,因为其默认separators为(', ', ': ')
url
=
get_account_media_link
(
varibles
)
header
=
generate_header
(
)
response
=
requests
.
get
(
url
,
headers
=
header
,
proxies
=
proxies
)
media_json_data
=
json
.
loads
(
response
.
text
)
media_raw_data
=
media_json_data
[
'data'
[
'user'
[
'edge_owner_to_timeline_media'
[
'edges'
if
not
media_raw_data
:
return
medias
for
item
in
media_raw_data
:
if
index
==
count
:
return
medias index
+=
1
medias
.
append
(
general_resolve_media
(
item
[
'node'
)
)
max_id
=
media_json_data
[
'data'
[
'user'
[
'edge_owner_to_timeline_media'
[
'page_info'
[
'end_cursor'
has_next_page
=
media_json_data
[
'data'
[
'user'
[
'edge_owner_to_timeline_media'
[
'page_info'
[
'has_next_page'
return
medias
def
get_media_by_url
(
media_url
)
:
response
=
requests
.
get
(
get_media_url
(
media_url
)
,
proxies
=
proxies
,
headers
=
generate_header
(
)
)
media_json
=
json
.
loads
(
response
.
text
)
return
general_resolve_media
(
media_json
[
'graphql'
[
'shortcode_media'
)
def
get_account_media_link
(
varibles
)
:
return
ACCOUNT_MEDIAS
%
urllib
.
parse
.
quote
(
varibles
)
def
get_account_link
(
user_name
)
:
return
ACCOUNT_PAGE
%
user_name
def
get_media_url
(
media_url
)
:
return
media_url
.
rstrip
(
'/'
)
+
'/?__a=1'
# def generate_instagram_gis(varibles):
# rhx_gis = get_rhx_gis()
# gis_token = rhx_gis + ':' + varibles
# x_instagram_token = hashlib.md5(gis_token.encode('utf-8')).hexdigest()
# return x_instagram_token
def
generate_header
(
gis_token
=
''
)
:
# todo: if have session, add the session key:value to header
header
=
{
'user-agent'
:
USER_AGENT
,
}
if
gis_token
:
header
[
'x-instagram-gis'
=
gis_token
return
header
def
general_resolve_media
(
media
)
:
res
=
{
'id'
:
media
[
'id'
,
'type'
:
media
[
'__typename'
[
5
:
.
lower
(
)
,
'content'
:
media
[
'edge_media_to_caption'
[
'edges'
[
0
[
'node'
[
'text'
,
'title'
:
'title'
in
media
and
media
[
'title'
or
''
,
'shortcode'
:
media
[
'shortcode'
,
'preview_url'
:
BASE_URL
+
'/p/'
+
media
[
'shortcode'
,
'comments_count'
:
media
[
'edge_media_to_comment'
[
'count'
,
'likes_count'
:
media
[
'edge_media_preview_like'
[
'count'
,
'dimensions'
:
'dimensions'
in
media
and
media
[
'dimensions'
or
{
}
,
'display_url'
:
media
[
'display_url'
,
'owner_id'
:
media
[
'owner'
[
'id'
,
'thumbnail_src'
:
'thumbnail_src'
in
media
and
media
[
'thumbnail_src'
or
''
,
'is_video'
:
media
[
'is_video'
,
'video_url'
:
'video_url'
in
media
and
media
[
'video_url'
or
''
}
return
res
def
resolve_account_data
(
account_data
)
:
account
=
{
'country'
:
account_data
[
'country_code'
,
'language'
:
account_data
[
'language_code'
,
'biography'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'biography'
,
'followers_count'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'edge_followed_by'
[
'count'
,
'follow_count'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'edge_follow'
[
'count'
,
'full_name'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'full_name'
,
'id'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'id'
,
'is_private'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'is_private'
,
'is_verified'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'is_verified'
,
'profile_pic_url'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'profile_pic_url_hd'
,
'username'
:
account_data
[
'entry_data'
[
'ProfilePage'
[
0
[
'graphql'
[
'user'
[
'username'
,
}
return
accountaccount
=
get_account
(
'shaq'
)
result
=
get_media_by_user_id
(
account
[
'id'
,
56
)
media
=
get_media_by_url
(
'https://www.idiancai.com/p/Bw3-Q2XhDMf/'
)
print
(
len
(
result
)
)
print
(
result
)

封装成库了!

除此以外,为了方便我写了一个库放在了 github 上,里面包含了很多操作,希望大家能看一下给点建议。如果对你有用的话,欢迎 star 和 PR~ 感谢泥萌!!

更多学习资料可关注:itheimaGZ获取