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 ⛔️.
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)
| image_url | description | n_comments | n_likes | timestamp | |
---|---|---|---|---|---|---|
0 | CbrYIabMBXS | Różany, Gronowo Elbląskie, woj. warmińsko-mazu... | 14 | 475 | 1648535479 | |
1 | CbnRiJwsxsc | Komorów, Michałowice, woj. mazowieckie \nCena:... | 28 | 761 | 1648397802 | |
2 | CbhVQU3MtTR | Pomorowo, Lidzbark Warmiński, woj. warmińsko-m... | 14 | 526 | 1648198427 | |
3 | CbakX60Me4r | Smyków, Radgoszcz, woj. małopolskie \nCena: 37... | 10 | 264 | 1647971472 | |
4 | CbXGs-JNK0U | 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)]
| 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 | 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 | 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 | 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 | 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 | 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 | 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.