Przejdź do głównej zawartości

Scrapowanie Instagrama

Mateusz Buda
Paweł Kobojek

Ten wpis jest kompleksowym samouczkiem dotyczącym zbierania informacji i postów z publicznego profilu na Instagramie za pomocą API Scraping Fish. Będziemy scrapować posty z profilu, który zawiera listę starych domów na sprzedaż, aby znaleźć najlepszą ofertę.

Przygotowaliśmy do tego wpisu pythonowy notebook udostępniony na GitHubie: instagram-scraping-fish. Aby móc go uruchomić i faktycznie zescrapować dane, potrzebujesz klucz do API Scraping Fish, o który możesz poprosić tutaj: Scraping Fish Kontakt. Otrzymasz darmowy pakiet startowy zawierający 1000 zapytań, aby uruchomić ten samouczek i samodzielnie wypróbować API. Bez klucza do API Scraping Fish prawdopodobnie zostaniesz natychmiast zablokowany ⛔️.

info

Należy zaznaczyć, że do scrapowania używamy prywatnego (nieudokumentowanego) API Instagrama, a udostępniany przez nas kod działa na stan z kwietnia 2022 roku. Jeśli Instagram zmieni coś w swoim API, na którym polegamy, ten samouczek może przestać działać i będzie musiał zostać dostosowany. Jeśli napotkasz jakiś problem, otwórz nowy issue na GitHubie, a my go zbadamy.

Przykład zastosowania

Jako przykład, aby przetestować możliwości Scraping Fish do scrapowania Instagrama, pobierzemy i przeanalizujemy dane z postów udostępnianych przez publiczny profil Stare domy 🏚. Zawiera on zbiorcze zestawienie ogłoszeń sprzedaży starych domów w Polsce. Opisy postów na tym profilu zawierają częściowo ustrukturyzowane dane o nieruchomościach, w tym lokalizację, cenę, rozmiar itp.

Endpoint do profilu na Instagramie

Pierwszym endpointem, z którego skorzystamy do pobrania danych o profilu jest: https://www.instagram.com/staredomynasprzedaz/?__a=1. W odpowiedzi dostaniemy JSONa zawierającego:

  • identyfikator użytkownika potrzebny do kolejnych zapytań,
  • ogólne informacje o profilu (nieużywane w tym wpisie, ale dostępne w odpowiedzi),
  • pierwsza strona postów i
  • kursor następnej strony, aby pobrać kolejną partię postów.

Paginacja przy użyciu Instagram GraphQL API

Dla następnych zapytań o uzyskanie kolejnych stron z postami użyjemy API GraphQL Instagrama, który wymaga identyfikatora użytkownika i kursora z poprzedniej odpowiedzi: https://instagram.com/graphql/query/?query_id=17888483320059182&id={user_id} &first=24&after={end_cursor}

Parametr query_id jest ustalony na wartość 17888483320059182. Nie musisz go zmieniać, wartość pozostaje taka sama niezależnie od profilu na Instagramie i strony z postami użytkowników.

Możesz przetestować różne wartości dla parametru zapytania first, aby pobrać większą liczbę postów na stronę niż 24 ustawione w kodzie i tym samym zmniejszyć całkowitą liczbę zapytań. Pamiętaj jednak, że użycie zbyt dużej wartości może wyglądać podejrzanie i skutkować zwróceniem strony logowania na Instagramie zamiast prawidłowego JSONa.

Aby uzyskać informacje o następnej stronie z odpowiedzi, użyjemy następującej funkcji:

def parse_page_info(response_json: Dict[str, Any]) -> Dict[str, Union[Optional[bool], Optional[str]]]:
top_level_key = "graphql" if "graphql" in response_json else "data"
user_data = response_json[top_level_key].get("user", {})
page_info = user_data.get("edge_owner_to_timeline_media", {}).get("page_info", {})
return page_info

Parsowanie postów ze zwróconego JSONa

Teraz, kiedy mamy już gotowe zapytania o profil użytkownika i stron z postami, możemy zaimplementować funkcję parsującą odpowiedź w celu pobrania potrzebnych nam informacji o poście.

Struktura odpowiedzi z obu opisanych powyżej linków jest ogólnie taka sama, ale pierwszym kluczem w słowniku do obiektu dla odpowiedzi o profil jest graphql, podczas gdy dla zapytania GraphQL jest to data. Uwzględniliśmy to już w kodzie parsowania informacji o następnej stronie z postami.

Funkcja, która pobiera podstawowe informacje o wpisie, polega po prostu na wyciągnięciu odpowiednich kluczy z JSONa odpowiedzi:

def parse_posts(response_json: Dict[str, Any]) -> List[Dict[str, Any]]:
top_level_key = "graphql" if "graphql" in response_json else "data"
user_data = response_json[top_level_key].get("user", {})
post_edges = user_data.get("edge_owner_to_timeline_media", {}).get("edges", [])
posts = []
for node in post_edges:
post_json = node.get("node", {})
shortcode = post_json.get("shortcode")
image_url = post_json.get("display_url")
caption_edges = post_json.get("edge_media_to_caption", {}).get("edges", [])
description = caption_edges[0].get("node", {}).get("text") if len(caption_edges) > 0 else None
n_comments = post_json.get("edge_media_to_comment", {}).get("count")
likes_key = "edge_liked_by" if "edge_liked_by" in post_json else "edge_media_preview_like"
n_likes = post_json.get(likes_key, {}).get("count")
timestamp = post_json.get("taken_at_timestamp")
posts.append({
"shortcode": shortcode,
"image_url": image_url,
"description": description,
"n_comments": n_comments,
"n_likes": n_likes,
"timestamp": timestamp,
})
return posts

Zwracana jest lista słowników reprezentujących posty, które zawierają:

  • shortcode: możesz go użyć, aby przejść do posta pod linkiem https://www.instagram.com/p/<shortcode>/
  • image_url: link to zdjęcia 🏞
  • description: opis pod postem 📝
  • n_comments: number of comments 💬
  • n_likes: liczba polubień 👍
  • timestamp: kiedy post został utworzony ⏰

Kompletna logika scrapowania Instagrama

Teraz jesteśmy gotowi do połączenia wszystkich elementów i możemy zaimplementować pełną logikę scrapowania profilu użytkownika na Instagramie, która pobiera wszystkie posty użytkowników, strona po stronie:

def scrape_ig_profile(username: str, url_prefix: str = "") -> List[Dict[str, Any]]:
# url in Scraping Fish API must be encoded: https://scrapingfish.com/docs/url-encoding
ig_profile_url = quote_plus(f"https://www.instagram.com/{username}/?__a=1")
response = requests.get(f"{url_prefix}{ig_profile_url}")
response.raise_for_status()
response_json = response.json()
# get user_id from response to request next pages with posts
user_id = response_json.get("graphql", {}).get("user", {}).get("id")
if not user_id:
print(f"User {username} not found.")
return []
# parse the first batch of posts from user profile response
posts = parse_posts(response_json=response_json)
# get next page cursor
page_info = parse_page_info(response_json=response_json)
end_cursor = page_info.get("end_cursor")
while end_cursor:
posts_url = quote_plus(
f"https://instagram.com/graphql/query/?query_id=17888483320059182&id={user_id}&first=24&after={end_cursor}"
)
response = requests.get(f"{url_prefix}{posts_url}")
response.raise_for_status()
try:
response_json = response.json()
posts.extend(parse_posts(response_json=response_json))
page_info = parse_page_info(response_json=response_json)
end_cursor = page_info.get("end_cursor")
except json.JSONDecodeError as e:
print(f"Instagram responded with invalid json, try again.")
continue
return posts

I to wszystko. Za pomocą tej funkcji możemy zescrapować wszystkie posty z dowolnego publicznego profilu na Instagramie. W naszym przypadku będzie to staredomynasprzedaz:

url_prefix = f"https://scraping.narf.ai/api/v1/?api_key={API_KEY}&url="
posts = scrape_ig_profile(username="staredomynasprzedaz", url_prefix=url_prefix)

Dzięki API Scraping Fish powinno to zająć około 2 sekund na stronę, więc profil z 300 postami zostanie cescrapowany w około 25 sekund.

Ponieważ zwrócone posty są listą ustrukturyzowanych słowników, możemy utworzyć z nich tabelę przy pomocy biblioteki pandas dla łatwiejszego przetwarzania danych:

df = pd.DataFrame(posts)


shortcode

image_url

description

n_comments

n_likes

timestamp

0

CbrYIabMBXS

https://scontent-frt3-1.cdninstagram.com/v/t51...

Różany, Gronowo Elbląskie, woj. warmińsko-mazu...

14

475

1648535479

1

CbnRiJwsxsc

https://scontent-frt3-2.cdninstagram.com/v/t51...

Komorów, Michałowice, woj. mazowieckie \nCena:...

28

761

1648397802

2

CbhVQU3MtTR

https://scontent-frx5-2.cdninstagram.com/v/t51...

Pomorowo, Lidzbark Warmiński, woj. warmińsko-m...

14

526

1648198427

3

CbakX60Me4r

https://scontent-frt3-2.cdninstagram.com/v/t51...

Smyków, Radgoszcz, woj. małopolskie \nCena: 37...

10

264

1647971472

4

CbXGs-JNK0U

https://scontent-frt3-1.cdninstagram.com/v/t51...

Dębowa Łęka, Wschowa, woj. lubuskie\nCena: 389...

3

436

1647855253

...

...

...

...

...

...

...

Parsowanie cech nieruchomości z opisu postu

Z ustrukturyzowanej części opisu posta możemy pozyskać bardziej szczegółowe cechy nieruchomości:

  • lokalizacja (adres and województwo) 📍
  • cena w PLN 💰
  • rozmiar domu w m² 🏠
  • powierzchnia działki w m² 📐

Implementacja funkcji do parsowania opisu przy pomocy wyrażeń regularnych dostępna jest w pythonowym notebooku udostępnionym tutaj: https://github.com/mateuszbuda/instagram-scraping-fish/blob/master/instagram-tutorial.ipynb

Z tych informacji możemy w prosty sposób obliczyć dodatkowe cechy pochodne, np. cenę za m² domu i działki:

df["price_per_house_m2"] = df["price"].div(df["house_size"])
df["price_per_plot_m2"] = df["price"].div(df["plot_area"])

Eksploracja danych

Na podstawie utworzonej przez nas tabeli możemy wydobyć kilka przydatnych statystyk, np. liczbę domów w każdym województwie i średnią cenę za m²:

df.groupby("province").agg({"price_per_house_m2": ["mean", "count"]}).sort_values(by=("price_per_house_m2", "mean"))

province

price_per_house_m2

mean

count

opolskie

1643.662176

3

dolnośląskie

1749.198578

40

zachodniopomorskie

1873.457787

22

lubuskie

1997.868677

10

podkarpackie

2283.578380

46

łódzkie

2444.755891

7

podlaskie

2689.675717

46

warmińsko - mazurskie

2733.333333

1

lubelskie

2781.316515

18

małopolskie

2879.480040

47

śląskie

2969.714365

25

świętokrzyskie

3005.367271

2

wielkopolskie

3084.229161

7

warmińsko-mazurskie

3099.135703

29

pomorskie

3135.444546

8

kujawsko-pomorskie

3885.582011

6

mazowieckie

4167.280252

20

Możemy również filtrować dane, aby znaleźć domy, które mogą nas zainteresować. Przykład poniżej szuka domów wystawionych za cenę mniejszą niż 200.000 zł i powierzchni od 100 m² do 200 m². Oto link do jednego z nich na podstawie jego shortcode'u: https://www.instagram.com/p/CYv93e8Nvwh/

df[(df["price"] < 200000.0) & (df["house_size"] < 200.0) & (df["house_size"] > 100.0)]


shortcode

image_url

description

n_comments

n_likes

timestamp

address

province

price

house_size

plot_area

price_per_house_m2

price_per_plot_m2

31

CYv93e8Nvwh

https://scontent-frt3-1.cdninstagram.com/v/t51...

Ponikwa, Bystrzyca Kłodzka, woj. dolnośląskie ...

23

701

1642247030

Ponikwa, Bystrzyca Kłodzka

dolnośląskie

165000.0

120.00

3882.0

1375.000000

42.503864

57

CXGiAE6sw6y

https://scontent-frx5-1.cdninstagram.com/v/t51...

Gadowskie Holendry, Tuliszków, woj. wielkopols...

7

462

1638709205

Gadowskie Holendry, Tuliszków

wielkopolskie

199000.0

111.00

3996.0

1792.792793

49.799800

122

CTSiKe4sGsx

https://scontent-frx5-1.cdninstagram.com/v/t51...

Gotówka, Ruda - Huta, woj. lubelskie\nCena: 18...

3

189

1630522009

Gotówka, Ruda - Huta

lubelskie

186000.0

120.00

1832.0

1550.000000

101.528384

149

CR040yusVyV

https://scontent-vie1-1.cdninstagram.com/v/t51...

Leżajsk, woj. podkarpackie \nCena: 175 000 zł\...

26

547

1627379773

Leżajsk

podkarpackie

175000.0

108.00

912.0

1620.370370

191.885965

181

CQvirvJM0vi

https://scontent-vie1-1.cdninstagram.com/v/t51...

Rząśnik, Świerzawa, woj. dolnośląskie \nCena: ...

4

239

1625052909

Rząśnik, Świerzawa

dolnośląskie

199000.0

160.00

1900.0

1243.750000

104.736842

190

CQhJJTFsyPt

https://scontent-vie1-1.cdninstagram.com/v/t51...

Szymbark, Gorlice, woj. małopolskie \nCena: 19...

2

222

1624569758

Szymbark, Gorlice

małopolskie

199000.0

136.00

9574.0

1463.235294

20.785461

...

...

...

...

...

...

...

...

...

...

...

...

...

...

Konkluzja

Mam nadzieję, że teraz czujesz się pewniej w scrapowaniu. Jak widać, bardzo łatwo jest scrapować publicznie dostępne dane za pomocą API Scraping Fish nawet z tak wymagających stron jak Instagram. W podobny sposób możesz przeglądać profile innych użytkowników, a także scrapować inne strony internetowe, które zawierają informacje istotne dla Ciebie lub Twojej firmy 📈.

Porozmawiajmy o Twoim przypadku 💼

Skontaktuj się z nami przy pomocy formularza kontaktowego. Możemy pomóc Ci w zintegrowaniu API Scraping Fish z istniejącym rozwiązaniem do scrapowania lub pomóc w stworzeniu systemu scrapującego dostosowanego do Twoich potrzeb.