tsmedia / laravel-instagram-scraper
Laravel package voor het scrapen van publieke Instagram-profielen (eigen namespace, Guzzle 7 / PSR-18).
Package info
github.com/TS-MediaNL/laravel-instagram-scraper
pkg:composer/tsmedia/laravel-instagram-scraper
Requires
- php: ^8.2
- ext-curl: *
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.7
- illuminate/console: ^11.0|^12.0|^13.0|^14.0|^15.0
- illuminate/contracts: ^11.0|^12.0|^13.0|^14.0|^15.0
- illuminate/support: ^11.0|^12.0|^13.0|^14.0|^15.0
- psr/http-client: ^1.0.1
- psr/http-factory: ^1.0.2
- psr/simple-cache: >=1.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0|^12.0|^13.0
- phpunit/phpunit: ^11.0|^12.0
This package is auto-updated.
Last update: 2026-05-14 23:00:38 UTC
README
Een Laravel package om publieke Instagram-profielen te scrapen via HTTP.
Gebouwd op Laravel's eigen HTTP client (PSR-18 adapter), met automatische retry, proxy-ondersteuning en volledige integratie in het Laravel service-container systeem.
Vereisten
| Vereiste | Versie |
|---|---|
| PHP | ^8.2 |
| Laravel | ^11.0 | ^12.0 |
| ext-json | * |
| ext-curl | * |
Installatie
composer require tsmedia/laravel-instagram-scraper
Laravel registreert de service provider en de InstagramProfile facade automatisch via package auto-discovery.
Config publiceren (optioneel)
php artisan vendor:publish --tag=instagram-scraper-config
Dit plaatst config/instagram-scraper.php in je project zodat je alle opties kunt aanpassen.
Configuratie
Voeg de gewenste waarden toe aan je .env:
# Timeouts (seconden) INSTAGRAM_SCRAPER_TIMEOUT=60 INSTAGRAM_SCRAPER_CONNECT_TIMEOUT=15 # Automatische retry bij fouten INSTAGRAM_SCRAPER_RETRY_MAX=3 # Totaal aantal pogingen (1 = geen retry) INSTAGRAM_SCRAPER_RETRY_DELAY_MS=1000 # Basisvertraging in ms (wordt verdubbeld per poging) # Optioneel: vaste user-agent INSTAGRAM_SCRAPER_USER_AGENT= # Optioneel: HTTP proxy INSTAGRAM_SCRAPER_PROXY=http://user:pass@proxy.example.com:8080 # Optioneel: authenticatie (zie sectie hieronder) INSTAGRAM_SCRAPER_SESSION_ID= INSTAGRAM_SCRAPER_USERNAME= INSTAGRAM_SCRAPER_PASSWORD=
Authenticatie (optioneel maar aanbevolen)
Zonder authenticatie werkt de package publiek via de web_profile_info endpoint. Dit geeft:
- Profiel-informatie (volgers, bio, etc.)
- Maximaal ~12 recentste grid-posts
Met een ingelogde sessie zijn ook beschikbaar:
- Meer dan 12 posts (paginering)
- Reels en trial reels
- Privé-profielen (als je ze volgt)
Methode 1 — Session ID (aanbevolen)
Dit is de stabielste aanpak: geen wachtwoord in je .env, werkt met 2FA-accounts en is moeilijker te detecteren door Instagram.
Hoe haal je de session ID op:
- Open instagram.com in je browser en log in
- Open DevTools (
F12) → Application → Cookies →https://www.instagram.com - Zoek de cookie met de naam
sessionid - Kopieer de waarde en zet hem in je
.env:
INSTAGRAM_SCRAPER_SESSION_ID=54524509719%3AaBcDeFgHiJkLmN%3A12%3AAbCdEfGhIjKlMnOpQrStUvWxYz
De sessie is geldig totdat je uitlogt of na ±90 dagen. Gebruik een apart scraper-account, niet je persoonlijke account.
Methode 2 — Username + Password
De package logt automatisch in bij de eerste request en cachet de sessie.
INSTAGRAM_SCRAPER_USERNAME=mijn_scraper_account INSTAGRAM_SCRAPER_PASSWORD=mijn_wachtwoord
Let op: Werkt niet bij accounts met twee-factor-authenticatie (2FA). Instagram kan inlogpogingen ook blokkeren bij verdacht gebruik.
Handmatig inloggen (runtime)
Je kunt ook buiten de config om inloggen, bijvoorbeeld als je meerdere accounts wilt rouleren:
use TsMedia\LaravelInstagramScraper\Facades\InstagramProfile; // Met session ID InstagramProfile::loginWithSessionId('jouw_session_id'); // Of met username + password InstagramProfile::login(); // Check of je ingelogd bent if (InstagramProfile::isLoggedIn()) { // ... }
Retry-gedrag
De retry gebruikt exponentiële backoff: bij 3 pogingen en 1000ms basisvertraging zijn de wachttijden 1s → 2s → 4s.
Standaard wordt opnieuw geprobeerd bij statuscodes: 429, 500, 502, 503, 504 en bij verbindingsfouten.
Package testen (smoke / live)
In de root van deze repository (na composer install):
# Alleen unit-tests (geen live Instagram) composer test # Live smoke-test (HTTP naar instagram.com; kan skippen bij 400/429 enz.) composer test:network
Of met een andere publieke gebruikersnaam:
php scripts/smoke-test.php nasa
De variabele INSTAGRAM_SMOKE_USERNAME (via tweede argument in smoke-test.php) bepaalt welk account wordt opgevraagd; standaard is instagram.
In een Laravel-app (na composer require)
php artisan instagram-scraper:test php artisan instagram-scraper:test --username=nasa --timeline
Gebruik
Via de InstagramProfile facade
De makkelijkste manier — gebruik dit in controllers, jobs en commands:
use TsMedia\LaravelInstagramScraper\Facades\InstagramProfile; // Accountinformatie ophalen $account = InstagramProfile::accountByUsername('nasa'); echo $account->getUsername(); // nasa echo $account->getFullName(); // NASA echo $account->getBiography(); // Explore the universe... echo $account->getFollowersCount(); // 97000000 echo $account->getMediaCount(); // 4200 echo $account->getProfilePicUrl(); // https://... echo $account->isPrivate(); // false echo $account->isVerified(); // true
Via dependency injection
Aanbevolen in services en repositories — beter testbaar:
use TsMedia\LaravelInstagramScraper\InstagramProfileClient; class InstagramService { public function __construct( private readonly InstagramProfileClient $instagram, ) {} public function getProfile(string $username): array { $account = $this->instagram->accountByUsername($username); return [ 'username' => $account->getUsername(), 'followers' => $account->getFollowersCount(), 'is_private' => $account->isPrivate(), ]; } }
Alle beschikbare methoden
accountByUsername(string $username): Account
Haal volledig account op via gebruikersnaam. Gooit InstagramNotFoundException als het account niet bestaat.
$account = InstagramProfile::accountByUsername('natgeo'); $account->getId(); // numerieke user-ID (string) $account->getUsername(); $account->getFullName(); $account->getBiography(); $account->getWebsite(); $account->getFollowersCount(); $account->getFollowsCount(); $account->getMediaCount(); $account->getProfilePicUrl(); $account->getProfilePicUrlHd(); $account->isPrivate(); $account->isVerified();
accountOrNull(string $username): ?Account
Zoals accountByUsername, maar geeft null terug als het account niet bestaat — handig voor bulk-checks:
$account = InstagramProfile::accountOrNull('might_not_exist'); if ($account === null) { // account bestaat niet of is privé }
timelineByUserId(int $userId, int $count = 24, string $maxId = ''): array
Haal de tijdlijn op van een specifiek account via de numerieke user-ID.
Gebruik $maxId (de id van de laatste Media) voor paginering.
$medias = InstagramProfile::timelineByUserId(528817151, count: 12); foreach ($medias as $media) { echo $media->getShortCode(); // Bxy123abc echo $media->getType(); // image | video | sidecar echo $media->getCaption(); // onderschrift echo $media->getLikesCount(); echo $media->getCommentsCount(); echo $media->getCreatedTime(); // Unix timestamp echo $media->getImageHighResolutionUrl(); // thumbnail URL echo $media->getLink(); // https://www.instagram.com/p/Bxy123abc/ } // Tweede pagina laden $lastId = end($medias)->getId(); $page2 = InstagramProfile::timelineByUserId(528817151, count: 12, maxId: $lastId);
timelineByUsername(string $username, int $count = 24): array
Korte variant als je alleen de gebruikersnaam weet (haalt userId intern op):
$medias = InstagramProfile::timelineByUsername('nasa', count: 9);
mediasByTag(string $tag, int $count = 24): array
Recente posts voor een hashtag:
$medias = InstagramProfile::mediasByTag('amsterdam', count: 15); foreach ($medias as $media) { echo $media->getShortCode(); echo $media->getCaption(); }
mediaByShortCode(string $shortCode): Media
Één post ophalen via de shortcode (het stuk in de URL na /p/):
// URL: https://www.instagram.com/p/Bxy123abc/ $media = InstagramProfile::mediaByShortCode('Bxy123abc'); echo $media->getId(); echo $media->getCaption(); echo $media->getType(); // image | video | sidecar echo $media->getVideoUrl(); // bij type video
commentsByShortCode(string $shortCode, int $count = 20, string $maxId = ''): array
Comments van een post ophalen:
$comments = InstagramProfile::commentsByShortCode('Bxy123abc', count: 50); foreach ($comments as $comment) { echo $comment->getText(); echo $comment->getCreatedAt(); echo $comment->getOwner()->getUsername(); }
highlightsByUserId(int $userId): array
Story highlights van een account:
$highlights = InstagramProfile::highlightsByUserId(528817151); foreach ($highlights as $highlight) { echo $highlight->getTitle(); // 'Behind the scenes' echo $highlight->getCoverUrl(); // thumbnail }
locationById(int $locationId): Location
Locatie-informatie op basis van een Facebook locatie-ID:
$location = InstagramProfile::locationById(213385402); echo $location->getName(); echo $location->getLat(); echo $location->getLng();
mediasByLocationId(int $locationId, int $count = 12): array
Recente posts bij een locatie:
$medias = InstagramProfile::mediasByLocationId(213385402, count: 9);
engine(): Instagram
Directe toegang tot de volledige scraper-engine voor geavanceerde operaties:
$engine = InstagramProfile::engine(); // Volgers ophalen (vereist login) $followers = $engine->getFollowers($userId, $count = 100); // Zoeken op tag $tags = $engine->searchTagsByTagName('amsterdam'); // Inloggen met sessie $engine->login();
MediaPayloadFactory
Transformeer Media-objecten naar een genormaliseerd array-formaat (compatibel met RocketAPI-structuur):
use TsMedia\LaravelInstagramScraper\Support\MediaPayloadFactory; $medias = InstagramProfile::timelineByUserId(528817151, count: 24); // Alle video's als clip-payload $clips = MediaPayloadFactory::videoClipItemsFromMedias($medias); foreach ($clips as $clip) { $clip['media']['pk']; // ID $clip['media']['code']; // shortcode $clip['media']['play_count']; // views $clip['media']['like_count']; $clip['media']['comment_count']; $clip['media']['caption']['text']; $clip['media']['image_versions2']['candidates'][0]['url']; // thumbnail } // Opzoektabel: pk → true (voor snelle deduplicatie) $seen = MediaPayloadFactory::feedPkLookupFromMedias($medias); if (isset($seen[$someMediaId])) { // al verwerkt } // Eén media naar clip-formaat $clip = MediaPayloadFactory::mediaToClipItem($medias[0]);
Foutafhandeling
Alle exceptions staan in TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\:
use TsMedia\LaravelInstagramScraper\Facades\InstagramProfile; use TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\InstagramAuthException; use TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\InstagramNotFoundException; use TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\InstagramAgeRestrictedException; use TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\InstagramException; use TsMedia\LaravelInstagramScraper\InstagramScraper\Http\NetworkException; try { $account = InstagramProfile::accountByUsername($username); } catch (InstagramNotFoundException $e) { // Account bestaat niet Log::warning("Instagram account niet gevonden: {$username}"); } catch (InstagramAgeRestrictedException $e) { // Account is leeftijdsbeperkt (403) Log::info("Leeftijdsbeperkt account: {$username}"); } catch (InstagramAuthException $e) { // Authenticatie vereist of sessie verlopen (401) Log::error('Instagram auth fout: ' . $e->getMessage()); } catch (NetworkException $e) { // Geen verbinding (DNS, timeout, proxy) Log::error('Netwerkfout: ' . $e->getMessage()); } catch (InstagramException $e) { // Algemene Instagram fout — bevat HTTP-code en response body Log::error("Instagram fout [{$e->getHttpCode()}]: {$e->getMessage()}"); Log::debug('Response body: ' . $e->getResponseBody()); }
Gebruik in een Laravel Job
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use TsMedia\LaravelInstagramScraper\InstagramProfileClient; use TsMedia\LaravelInstagramScraper\InstagramScraper\Exception\InstagramNotFoundException; class SyncInstagramProfile implements ShouldQueue { use Dispatchable, Queueable; public int $tries = 3; public int $backoff = 60; public function __construct( public readonly string $username, ) {} public function handle(InstagramProfileClient $instagram): void { $account = $instagram->accountOrNull($this->username); if ($account === null) { return; } \DB::table('instagram_profiles')->updateOrInsert( ['username' => $account->getUsername()], [ 'full_name' => $account->getFullName(), 'followers_count' => $account->getFollowersCount(), 'media_count' => $account->getMediaCount(), 'is_verified' => $account->isVerified(), 'synced_at' => now(), ], ); } }
Gebruik in een Artisan Command
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use TsMedia\LaravelInstagramScraper\Facades\InstagramProfile; use TsMedia\LaravelInstagramScraper\Support\MediaPayloadFactory; class FetchInstagramReels extends Command { protected $signature = 'instagram:reels {username} {--count=12}'; protected $description = 'Haal recente reels op voor een Instagram-account'; public function handle(): int { $username = $this->argument('username'); $count = (int) $this->option('count'); $this->info("Account ophalen: {$username}"); $account = InstagramProfile::accountByUsername($username); $userId = (int) $account->getId(); $this->info("Timeline ophalen ({$count} posts)..."); $medias = InstagramProfile::timelineByUserId($userId, $count); $clips = MediaPayloadFactory::videoClipItemsFromMedias($medias); $this->table( ['Shortcode', 'Views', 'Likes'], array_map(fn($clip) => [ $clip['media']['code'], number_format($clip['media']['play_count']), number_format($clip['media']['like_count']), ], $clips), ); $this->info(count($clips) . ' video(s) gevonden.'); return self::SUCCESS; } }
Testen (Http::fake)
Omdat de package Laravel's HTTP client gebruikt kun je verzoeken mocken met Http::fake():
use Illuminate\Support\Facades\Http; use TsMedia\LaravelInstagramScraper\Facades\InstagramProfile; Http::fake([ 'www.instagram.com/api/v1/users/web_profile_info/*' => Http::response( file_get_contents(base_path('tests/fixtures/instagram_account.json')), 200, ), ]); $account = InstagramProfile::accountByUsername('nasa'); Http::assertSent(fn ($request) => str_contains($request->url(), 'web_profile_info') );
HTTP-integratie
De package gebruikt Laravel's eigen HTTP client (Illuminate\Http\Client) als PSR-18 adapter.
Dat betekent:
- Logging: alle requests verschijnen automatisch in Laravel Telescope / Debugbar
- Fake/Mock:
Http::fake()werkt out-of-the-box in tests - Events:
RequestSending,ResponseReceived,ConnectionFailedworden gefired - Macros: je kunt eigen macros op de HTTP client registreren
Configuratie-referentie
// config/instagram-scraper.php return [ 'http' => [ 'timeout' => 60, // Maximale request-tijd in seconden 'connect_timeout' => 15, // Maximale verbindingstijd in seconden 'http_errors' => false, // Geen exceptions op HTTP-fouten (scraper doet eigen afhandeling) ], 'retry' => [ 'max_attempts' => 3, // Totaal aantal pogingen 'delay_ms' => 1000, // Basisvertraging (exponentieel) 'on_codes' => [429, 500, 502, 503, 504], // Codes die retry triggeren ], 'user_agent' => null, // Overschrijf de standaard user-agent (null = gebruik ingebouwde) 'proxy' => null, // HTTP-proxy URL (null = geen proxy) ];
Licentie
MIT — © 2026 TS-Media