feat: implement automated media monitoring crawler with Python script and Laravel console command

This commit is contained in:
Yoga Pangestu 2026-04-11 13:47:18 +07:00
parent ca5aa283d0
commit 4a1993a778
5 changed files with 1044 additions and 1 deletions

View File

@ -0,0 +1,130 @@
<?php
namespace App\Console\Commands;
use App\Enums\Channel;
use App\Models\MediaMonitoring;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class CrawlMediaMonitoringCommand extends Command
{
protected $signature = 'monitoring:crawl {--limit=20 : Maximum number of news to save globally}';
protected $description = 'Crawl data from Google News RSS based on active Website Themes or specific keywords';
protected $keywords = [
'Purwakarta',
'Pemkab Purwakarta',
'Bupati Purwakarta',
'Pendidikan',
'Pariwisata',
'Kriminalitas',
'Institusi Pemerintah/ Dinas',
'Diskominfo Purwakarta',
'Om Zein',
'Abang Ijo Hapidin',
'Wakil Bupati Purwakarta',
];
public function handle()
{
$globalLimit = (int) $this->option('limit');
$totalImported = 0;
$this->info('Starting crawler engine...');
$keywordsToCrawl = $this->keywords;
shuffle($keywordsToCrawl);
foreach ($keywordsToCrawl as $kw) {
$offset = 0;
while ($totalImported < $globalLimit) {
$remainingLimit = $globalLimit - $totalImported;
$this->info("\n--- Crawling: $kw (limit: $remainingLimit, offset: $offset) ---");
$command = ['python3', base_path('scripts/crawler.py'), '--keyword', $kw, '--limit', $remainingLimit, '--offset', $offset];
$process = new Process($command);
$process->setTimeout(600);
$process->run(function ($type, $buffer) {
if ($type === Process::ERR) {
$this->output->write($buffer);
}
});
if (! $process->isSuccessful()) {
$this->error($process->getErrorOutput());
break;
}
$output = $process->getOutput();
$newsData = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($newsData)) {
$this->warn("No more news found for '$kw'.");
break;
}
$importedCount = $this->saveToDatabase($newsData, $remainingLimit);
$totalImported += $importedCount;
$offset += $remainingLimit;
}
if ($totalImported >= $globalLimit) {
$this->info("\n✅ Reached global limit of $globalLimit new articles. Stopping crawl.");
break;
}
}
}
protected function saveToDatabase(array $newsData, int $maxToSave)
{
$count = 0;
foreach ($newsData as $item) {
if ($count >= $maxToSave) {
break;
}
if (MediaMonitoring::where('link', $item['url'])->where('title', $item['title'])->exists()) {
continue;
}
$host = parse_url($item['url'], PHP_URL_HOST);
$mediaName = str_replace('www.', '', $host);
MediaMonitoring::create([
'code' => $this->code(),
'media_name' => $mediaName,
'title' => $item['title'],
'channel' => Channel::WEBSITE,
'writter' => $item['source'] ?? '-',
'link' => $item['url'],
'content' => $item['content'],
'keyword' => $item['keyword'],
'release_date' => Carbon::parse($item['published_at'])->toDateString(),
]);
$count++;
}
$this->info("Imported $count new articles.");
return $count;
}
protected function code()
{
$latestCode = MediaMonitoring::max('code');
$lastNumber = $latestCode
? intval(substr($latestCode, 3))
: 0;
return 'ADN'.str_pad($lastNumber + 1, 6, '0', STR_PAD_LEFT);
}
}

View File

@ -15,6 +15,7 @@
"barryvdh/laravel-dompdf": "^3.1",
"bezhansalleh/filament-shield": "^4.0",
"bilfeldt/laravel-route-statistics": "^4.2",
"crwlr/crawler": "^3.5",
"filament/filament": "^4.0",
"filament/spatie-laravel-media-library-plugin": "^4.0",
"filament/spatie-laravel-settings-plugin": "^4.4",

778
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "116c8e60c2cac8f2a9bac2ede4d16dea",
"content-hash": "b29f415e8e8d01a4f83dfbf06c166e32",
"packages": [
{
"name": "achyutn/filament-log-viewer",
@ -91,6 +91,60 @@
],
"time": "2026-01-30T17:48:57+00:00"
},
{
"name": "adbario/php-dot-notation",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/adbario/php-dot-notation.git",
"reference": "aa09db5a7a365a8d2d8dd7edfa3cd501d1a8acb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/adbario/php-dot-notation/zipball/aa09db5a7a365a8d2d8dd7edfa3cd501d1a8acb0",
"reference": "aa09db5a7a365a8d2d8dd7edfa3cd501d1a8acb0",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Adbar\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Riku Särkinen",
"email": "riku@adbar.io"
}
],
"description": "PHP dot notation access to arrays",
"homepage": "https://github.com/adbario/php-dot-notation",
"keywords": [
"ArrayAccess",
"dotnotation"
],
"support": {
"issues": "https://github.com/adbario/php-dot-notation/issues",
"source": "https://github.com/adbario/php-dot-notation/tree/3.5.0"
},
"time": "2026-04-04T04:29:49+00:00"
},
{
"name": "anourvalar/eloquent-serialize",
"version": "1.3.5",
@ -1251,6 +1305,142 @@
],
"time": "2024-07-16T11:13:48+00:00"
},
{
"name": "chrome-php/chrome",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/chrome-php/chrome.git",
"reference": "5b2ee677f34ed4cd9463e49dc59c4989d0fe40f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chrome-php/chrome/zipball/5b2ee677f34ed4cd9463e49dc59c4989d0fe40f3",
"reference": "5b2ee677f34ed4cd9463e49dc59c4989d0fe40f3",
"shasum": ""
},
"require": {
"chrome-php/wrench": "^1.8",
"evenement/evenement": "^3.0.1",
"monolog/monolog": "^1.27.1 || ^2.8 || ^3.2",
"php": "^7.4.15 || ^8.0.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.26",
"symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^9.6.3 || ^10.0.12",
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"HeadlessChromium\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Enrico Dias",
"email": "enrico@enricodias.com",
"homepage": "https://github.com/enricodias"
}
],
"description": "Instrument headless chrome/chromium instances from PHP",
"keywords": [
"browser",
"chrome",
"chromium",
"crawl",
"headless",
"pdf",
"puppeteer",
"screenshot"
],
"support": {
"issues": "https://github.com/chrome-php/chrome/issues",
"source": "https://github.com/chrome-php/chrome/tree/v1.15.0"
},
"time": "2025-12-27T12:23:40+00:00"
},
{
"name": "chrome-php/wrench",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/chrome-php/wrench.git",
"reference": "bd67a315cf2143ba30598339ad1b6c346db96c99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chrome-php/wrench/zipball/bd67a315cf2143ba30598339ad1b6c346db96c99",
"reference": "bd67a315cf2143ba30598339ad1b6c346db96c99",
"shasum": ""
},
"require": {
"ext-sockets": "*",
"php": "^7.4.15 || ^8.0.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/polyfill-php80": "^1.26"
},
"conflict": {
"wrench/wrench": "*"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^9.6.3 || ^10.0.12"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"Wrench\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "A simple PHP WebSocket implementation",
"keywords": [
"WebSockets",
"hybi",
"websocket"
],
"support": {
"issues": "https://github.com/chrome-php/wrench/issues",
"source": "https://github.com/chrome-php/wrench/tree/v1.8.0"
},
"time": "2025-12-27T11:29:35+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
@ -1407,6 +1597,472 @@
],
"time": "2025-08-20T19:15:30+00:00"
},
{
"name": "crwlr/crawler",
"version": "v3.5.6",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/crawler.git",
"reference": "d6680f9e698a050f3a5eded2fc8c6776d6043a3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/crawler/zipball/d6680f9e698a050f3a5eded2fc8c6776d6043a3b",
"reference": "d6680f9e698a050f3a5eded2fc8c6776d6043a3b",
"shasum": ""
},
"require": {
"adbario/php-dot-notation": "^3.1",
"chrome-php/chrome": "^1.7",
"crwlr/html-2-text": "^0.1.0",
"crwlr/robots-txt": "^1.1",
"crwlr/schema-org": "^0.2|^0.3",
"crwlr/url": "^2.1",
"crwlr/utils": "^1.2",
"ext-dom": "*",
"guzzlehttp/guzzle": "^7.4",
"php": "^8.1",
"psr/log": "^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"symfony/css-selector": "^6.0|^7.0",
"symfony/dom-crawler": "^6.0|^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.57",
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.0|^4.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.4|^2.0",
"phpstan/phpstan-mockery": "^1.0|^2.0",
"phpstan/phpstan-phpunit": "^1.0|^2.0",
"spatie/invade": "^2.0",
"symfony/process": "^6.0|^7.0"
},
"suggest": {
"ext-zlib": "Needed to uncompress compressed responses",
"voku/portable-ascii": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\Crawler\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Web crawling and scraping library.",
"homepage": "https://www.crwlr.software/packages/crawler",
"keywords": [
"bot",
"crawl",
"crawler",
"crawling",
"crwlr",
"scrape",
"scraper",
"scraping",
"web"
],
"support": {
"docs": "https://www.crwlr.software/packages/crawler",
"issues": "https://github.com/crwlrsoft/crawler/issues",
"source": "https://github.com/crwlrsoft/crawler"
},
"funding": [
{
"url": "https://github.com/sponsors/otsch",
"type": "github"
}
],
"time": "2026-01-05T11:13:18+00:00"
},
{
"name": "crwlr/html-2-text",
"version": "v0.1.2",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/html-2-text.git",
"reference": "5109fb909cee28d187ea14523bcdfca03b6e9e72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/html-2-text/zipball/5109fb909cee28d187ea14523bcdfca03b6e9e72",
"reference": "5109fb909cee28d187ea14523bcdfca03b6e9e72",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"masterminds/html5": "^2.8",
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.57",
"pestphp/pest": "^2.19|^3.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\Html2Text\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Convert HTML to formatted plain text.",
"homepage": "https://www.crwlr.software/packages/html-2-text",
"keywords": [
"converter",
"crwlr",
"html",
"html to text",
"text"
],
"support": {
"docs": "https://www.crwlr.software/packages/html-2-text",
"issues": "https://github.com/crwlrsoft/html-2-text/issues",
"source": "https://github.com/crwlrsoft/html-2-text"
},
"funding": [
{
"url": "https://github.com/sponsors/otsch",
"type": "github"
}
],
"time": "2024-11-06T17:55:46+00:00"
},
{
"name": "crwlr/query-string",
"version": "v1.0.3",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/query-string.git",
"reference": "4e71cae0c942fe1ce2ae22506bd46ecdfa5b4f0e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/query-string/zipball/4e71cae0c942fe1ce2ae22506bd46ecdfa5b4f0e",
"reference": "4e71cae0c942fe1ce2ae22506bd46ecdfa5b4f0e",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.8",
"pestphp/pest": "^1.21",
"phpstan/phpstan": "^1.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\QueryString\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "A library for convenient handling of query strings used in HTTP requests.",
"homepage": "https://www.crwlr.software/packages/query-string",
"keywords": [
"crwlr",
"http",
"query",
"query string",
"query-string",
"request",
"string"
],
"support": {
"docs": "https://www.crwlr.software/packages/query-string",
"issues": "https://github.com/crwlrsoft/query-string/issues",
"source": "https://github.com/crwlrsoft/query-string"
},
"time": "2023-04-12T21:31:58+00:00"
},
{
"name": "crwlr/robots-txt",
"version": "v1.1.2",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/robots-txt.git",
"reference": "420aea3518b9a4109defb72940a504d15ea0d26a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/robots-txt/zipball/420aea3518b9a4109defb72940a504d15ea0d26a",
"reference": "420aea3518b9a4109defb72940a504d15ea0d26a",
"shasum": ""
},
"require": {
"crwlr/url": "^1.0|^2.0",
"php": "^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.57",
"mockery/mockery": "^1.4",
"phpstan/phpstan": "^1.1",
"phpunit/phpunit": "^9.0",
"sempro/phpunit-pretty-print": "^1.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\RobotsTxt\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Robots Exclusion Standard/Protocol Parser for Web Crawling/Scraping",
"homepage": "https://www.crwlr.software/packages/robots-txt",
"keywords": [
"bot",
"bots",
"crawler",
"crwlr",
"exclusion",
"parser",
"protocol",
"robots",
"robots.txt",
"scraper",
"scraping",
"spider",
"standard"
],
"support": {
"docs": "https://www.crwlr.software/packages/robots-txt",
"issues": "https://github.com/crwlrsoft/robots-txt/issues",
"source": "https://github.com/crwlrsoft/robots-txt"
},
"funding": [
{
"url": "https://github.com/sponsors/otsch",
"type": "github"
}
],
"time": "2025-01-27T17:32:13+00:00"
},
{
"name": "crwlr/schema-org",
"version": "v0.3.4",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/schema-org.git",
"reference": "d55229470e641d1e3ecda3fc2e8b893d1ba2fad0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/schema-org/zipball/d55229470e641d1e3ecda3fc2e8b893d1ba2fad0",
"reference": "d55229470e641d1e3ecda3fc2e8b893d1ba2fad0",
"shasum": ""
},
"require": {
"crwlr/utils": "^1.0",
"php": "^8.0",
"psr/log": "^2.0|^3.0",
"spatie/schema-org": "~3.23.0",
"symfony/dom-crawler": "^6.0|^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.57",
"pestphp/pest": "^1.22|^2.0|^3.0",
"phpstan/phpstan": "^1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\SchemaOrg\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Extract schema.org structured data from HTML documents.",
"homepage": "https://www.crwlr.software/packages/schema-org",
"keywords": [
"JSON-LD",
"crwlr",
"data",
"org",
"schema",
"schema.org",
"structured"
],
"support": {
"docs": "https://www.crwlr.software/packages/schema-org",
"issues": "https://github.com/crwlrsoft/schema-org/issues",
"source": "https://github.com/crwlrsoft/schema-org"
},
"funding": [
{
"url": "https://github.com/sponsors/otsch",
"type": "github"
}
],
"time": "2025-04-08T11:22:10+00:00"
},
{
"name": "crwlr/url",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/url.git",
"reference": "227b673b7120197be8961050798426e9628dead3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/url/zipball/227b673b7120197be8961050798426e9628dead3",
"reference": "227b673b7120197be8961050798426e9628dead3",
"shasum": ""
},
"require": {
"crwlr/query-string": "^1.0",
"php": "^8.0",
"psr/http-message": "^1.0 || ^2.0",
"symfony/polyfill-intl-idn": "^1.11"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.4",
"pestphp/pest": "^1.22|^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.8|^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\Url\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Swiss Army knife for URLs.",
"homepage": "https://www.crwlr.software/packages/url",
"keywords": [
"UriInterface",
"components",
"crwlr",
"idn",
"parse",
"parser",
"parsing",
"psr-7",
"rfc3986",
"rfc3987",
"uri",
"url",
"urls"
],
"support": {
"docs": "https://www.crwlr.software/packages/url",
"issues": "https://github.com/crwlrsoft/url/issues",
"source": "https://github.com/crwlrsoft/url"
},
"time": "2026-01-06T19:06:56+00:00"
},
{
"name": "crwlr/utils",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/crwlrsoft/utils.git",
"reference": "c8b36051d44aa32f3108bd7408ed294c7eea9aab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crwlrsoft/utils/zipball/c8b36051d44aa32f3108bd7408ed294c7eea9aab",
"reference": "c8b36051d44aa32f3108bd7408ed294c7eea9aab",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.57",
"pestphp/pest": "^1.22|^2.0|^3.0",
"phpstan/phpstan": "^1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Crwlr\\Utils\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Olear",
"homepage": "https://www.otsch.codes",
"role": "Developer"
}
],
"description": "Utilities that are needed in multiple crawler packages.",
"keywords": [
"crwlr",
"json",
"utils"
],
"support": {
"issues": "https://github.com/crwlrsoft/utils/issues",
"source": "https://github.com/crwlrsoft/utils"
},
"time": "2024-12-08T14:20:52+00:00"
},
{
"name": "danharrin/date-format-converter",
"version": "v0.3.1",
@ -2088,6 +2744,53 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/igorw/evenement.git",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^9 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Evenement\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
}
],
"description": "Événement is a very simple event dispatching library for PHP",
"keywords": [
"event-dispatcher",
"event-emitter"
],
"support": {
"issues": "https://github.com/igorw/evenement/issues",
"source": "https://github.com/igorw/evenement/tree/v3.0.2"
},
"time": "2023-08-08T05:53:35+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
@ -8288,6 +8991,79 @@
],
"time": "2025-02-20T15:51:22+00:00"
},
{
"name": "spatie/schema-org",
"version": "3.23.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/schema-org.git",
"reference": "a8dc1b6fcdd06afc1ab084c3ead9b7a4c3d7a35d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/schema-org/zipball/a8dc1b6fcdd06afc1ab084c3ead9b7a4c3d7a35d",
"reference": "a8dc1b6fcdd06afc1ab084c3ead9b7a4c3d7a35d",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.6",
"graham-campbell/analyzer": "^4.2",
"illuminate/collections": "^8.62.0",
"league/flysystem": "^2.3.0 || ^3.0",
"pestphp/pest": "^1.21",
"symfony/console": "^5.3.7 || 6.0",
"twig/twig": "^3.3.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\SchemaOrg\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Tom Witkowski",
"email": "dev.gummibeer@gmail.com",
"homepage": "https://gummibeer.de",
"role": "Developer"
}
],
"description": "A fluent builder Schema.org types and ld+json generator",
"homepage": "https://github.com/spatie/schema-org",
"keywords": [
"schema-org",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/schema-org/issues",
"source": "https://github.com/spatie/schema-org/tree/3.23.1"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-01-31T14:54:12+00:00"
},
{
"name": "spatie/shiki-php",
"version": "2.3.3",

View File

@ -3,3 +3,4 @@
use Illuminate\Support\Facades\Schedule;
Schedule::command('cooperation:update-status')->daily();
Schedule::command('monitoring:crawl')->dailyAt('12:00');

135
scripts/crawler.py Normal file
View File

@ -0,0 +1,135 @@
import requests
from bs4 import BeautifulSoup
import xml.etree.ElementTree as ET
import json
import time
import sys
import argparse
from googlenewsdecoder import gnewsdecoder
HEADERS = {
"User-Agent": "Mozilla/5.0"
}
# ========================
# GENERIC PARSER
# ========================
def parse_content(html):
soup = BeautifulSoup(html, "html.parser")
decompose_tags = [
"script", "style", "nav", "footer", "header", "aside",
"form", "iframe", "noscript", "svg", "button", "menu",
"figure", "ins", "dialog", ".sidebar", "#sidebar"
]
for tag in soup(decompose_tags):
tag.decompose()
content_tags = ["p", "div", "article", "section", "span", "blockquote", "li", "td", "h1", "h2", "h3", "h4", "h5", "h6"]
block_tags = ["p", "div", "article", "section", "ul", "ol", "table", "blockquote"]
paragraphs = []
for tag in soup.find_all(content_tags):
if not tag.find(block_tags):
text = tag.get_text(separator=" ", strip=True)
if len(text) > 50:
paragraphs.append(text)
if not paragraphs:
text_blocks = soup.get_text(separator='\n').split('\n')
paragraphs = [t.strip() for t in text_blocks if len(t.strip()) > 50]
seen = set()
unique_paragraphs = []
for p_text in paragraphs:
if p_text not in seen:
seen.add(p_text)
unique_paragraphs.append(p_text)
return " ".join(unique_paragraphs)
# ========================
# RSS FETCH
# ========================
def get_rss(keyword):
url = f"https://news.google.com/rss/search?q={keyword}&hl=id&gl=ID&ceid=ID:id"
res = requests.get(url)
root = ET.fromstring(res.content)
return [
{
"title": item.find("title").text,
"link": item.find("link").text,
"pubDate": item.find("pubDate").text,
"source": item.find("source").text if item.find("source") is not None else "-"
}
for item in root.findall(".//item")
]
# ========================
# RESOLVE URL
# ========================
def resolve_url(url):
try:
res = requests.get(url, headers=HEADERS, timeout=10)
decoded = gnewsdecoder(res.url)
if decoded and decoded.get("status"):
final_url = decoded.get("decoded_url")
if final_url and "news.google.com" not in final_url:
return final_url
return None
except:
return None
# ========================
# CRAWLER
# ========================
def crawl(keyword="purwakarta", limit=5, offset=0):
results = []
for item in get_rss(keyword)[offset:offset+limit]:
real_url = resolve_url(item["link"]) or item["link"]
try:
res = requests.get(real_url, headers=HEADERS)
content = parse_content(res.text)
results.append({
"title": item["title"],
"url": real_url,
"published_at": item["pubDate"],
"content": content,
"keyword": keyword,
"source": item["source"]
})
print(f"{item['title']}", file=sys.stderr, flush=True)
time.sleep(1)
except Exception as e:
print(f"❌ error: {e}", file=sys.stderr, flush=True)
return results
# ========================
# MAIN
# ========================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--keyword", type=str, default="purwakarta")
parser.add_argument("--limit", type=int, default=1)
parser.add_argument("--offset", type=int, default=0)
args = parser.parse_args()
print(f"🔎 Crawling for keyword: {args.keyword} (Limit: {args.limit}, Offset: {args.offset})", file=sys.stderr, flush=True)
data = crawl(args.keyword, args.limit, args.offset)
print(json.dumps(data, ensure_ascii=False))