laravel-instagram-scraper maintained by tsmedia
Laravel Instagram Scraper
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