Laravel Crawler Data cực kì đơn giản với Goutte
Data là một trong những yếu tô quan trọng trong bất kì ứng dụng hay trang web nào. Đặc biệt trong các dự án mới luôn cần có dữ liệu để vận hành các chức năng. Nhưng vấn đề ở đây, dự án mới thì lấy đâu ra data?, mà việc nhập tay sẽ mất rất nhiều thời gian, dữ liệu lại không sát với thức tế. Vậy làm sao có thể có data? Crawler Data chính là câu trả lời cho bạn.
Crawler Data là tự động hóa việc trích xuất thông tin từ một nguồn nào đó, cụ thể ở bài viết này là chúng ta sẽ lấy thông tin các bài tin tức trên một website
1. Chuẩn bị
Chuẩn bị cho project laravel (Php, Composer, Mysql) và config kết nối đến database.
Chúng ta sẽ cài đặt package Laravel-goutte nên các bạn hãy đọc trước.
Địa chỉ website mẫu.
2. Cài đặt package
Đầu tiên cài đặt package bằng composer
$ composer require weidner/goutte
Sau đó kiểm tra trong composer.json
"require": {
"php": "^7.3|^8.0",
"laravel/framework": "^8.54",
"weidner/goutte": "^2.1"
},
Sau khi cài đặt thành công thì các bạn khai báo trong config/app.php
return [
'providers' => [
// ...
/*
* Package Service Providers...
*/
Weidner\Goutte\GoutteServiceProvider::class, // [1] This will register the Package in the laravel echo system
// ...
],
// ...
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
// ...
'Goutte' => Weidner\Goutte\GoutteFacade::class,
// ...
],
];
3. Migrations và Models
Để không làm project trở nên quá phức tạp, với mỗi bài viết, ta chỉ cần lấy thông tin là: title, content, description. Database tương ứng chỉ có một bảng duy nhất. Chạy lệnh sau để tạo 2 file model và migrations cho bảng posts.
$ php artisan make:model Post
Hàm up() trong file migration sẽ có nội dung như sau:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->longText('title');
$table->longText('content');
$table->longText('description');
$table->timestamps();
});
}
Sau đó để tạo bảng trong CSDL thì chúng ta sẽ dùng câu lênh:
$ php artisan migrate
4. Crawler Data
4.1 Bắt đầu
Trước tiên, ta tạo 1 command line, các bạn cũng có thể sử dụng route, controller để thực hiện. Ta sẽ xử lý logic luôn trong command line
Tạo command line:
$ php artisan make:command ScrapePost
Vào file \app\Console\Commands\ScrapePost.php và chúng ta sẽ thực hiện logic trong đó.
4.2 Tìm hiểu cấu trúc HTML
Để crawl dữ liệu từ một website, trước tiên ta phải hiểu cấu trúc HTML của website đó được tổ chức thế nào. Điều này cũng đồng nghĩa, nếu có bất kì cập nhật nào của chủ website, làm thay đổi cấu trúc HTML thì scraper của ta có khả năng ở nên vô dụng.
Truy cập vào link, kiểm tra các phần tử của website:
Một bài viết sẽ được chia làm 3 phần là title, description, content. Tất cả các bài ở cột bên trái mà chúng ta muốn cào sẽ có cấu trúc này. Trước tiên ta sẽ test cào phần title
Bạn F12 để inspect phần html của phần title. Phần title là thẻ h1 và có class là dt-news__title.
Tượng tự, bạn truy cập vào link để kiểm tra các phần description, content của website:
- Tất cả description đều được đặt giữa thẻ h2
- Content được năm trọn trong thẻ div có class "dt-news__content"
Lưu ý rằng cấu trúc HTML trên có thể đã thay đổi so với thời điểm bài viết được tạo ra, vì vậy, bạn nên tự mình vào link và tìm tòi. Đây có lẽ là công đoạn thú vị nhất trong toàn bộ quá trình.
4.3 Crawler Data thôi
Trước tiên, phần cào data này mình sẽ thực hiện login trên command line, các bạn có thể sử dụng route, controller để thực hiện.
Tạo command line
$ php artisan make:command ScrapePost
Truy cập vào thư mục /app/Console/Commands/ScrapePost.php để thực hiện logic trong đó.
Logic sẽ được xử lý trong handle(), link trong phần request GET là bài viết Người bán nhà phố để đi tìm "vùng xanh hạnh phúc"
Để test mình sẽ thực hiện lấy title của bài viết trước
// ScrapePost.php
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'scrape:post';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$crawler = GoutteFacade::request('GET', 'https://dantri.com.vn/lao-dong-viec-lam/can-tho-f0-f1-bot-lo-lang-vi-khong-phai-tra-vien-phi-20210829181940605.htm');
$title = $crawler->filter('h1.dt-news__title')->each(function ($node) {
return $node->text();
})[0];
print($title);
}
Phần protected $signature = 'command:name'; các bạn đổi tên thành 'scrape:post' nhé
Rồi bạn chạy command bằng terminal:
Vậy là đã mình đã thành công lấy được title bài viết, giờ mình sẽ lấy các phần còn lại nhé.
//handle
$title = $crawler->filter('h1.dt-news__title')->each(function ($node) {
return $node->text();
})[0];
$description = $crawler->filter('div.dt-news__sapo h2')->each(function ($node) {
return $node->text();
})[0];
$description = str_replace('Dân trí', '', $description);
$content = $crawler->filter('div.dt-news__content')->each(function ($node) {
return $node->text();
})[0];
Kết quả:
Ta đã lấy thành công thông tin bài viết. Vậy để lấy thêm được nhiều bài viết khác nữa thì sao? Ở phần chuẩn bị, các bạn thấy các bài viết đều có cấu trúc html như bài mình cào, vậy chỉ cần lấy link của các bài viết thì mình cũng sẽ lấy được nội dụng bài viết đó.
Phần href mà mình cần lấy nằm trong thẻ h3 có class là news-item__title, mình sẽ chọc vào thẻ h3 lấy thẻ a và attr là href.
public function handle()
{
$crawler = GoutteFacade::request('GET', 'https://dantri.com.vn/lao-dong-viec-lam.htm');
$linkPost = $crawler->filter('h3.news-item__title a')->each(function ($node) {
return $node->attr("href");
});
foreach ($linkPost as $link) {
print($link . "\n");
}
}
Mình đã lấy ra mảng linkPost chứa link những bài viết, vậy mình chỉ cần dùng vòng lặp chọc vào từng link một và xử lý bằng logic lúc ban đầu để lấy title, description, content là mình có thể lấy được được hết nội dung của đống bài viết này rồi.
public function handle()
{
$crawler = GoutteFacade::request('GET', 'https://dantri.com.vn/bat-dong-san.htm');
$linkPost = $crawler->filter('h3.news-item__title a')->each(function ($node) {
return $node->attr("href");
});
foreach ($linkPost as $link) {
$this->scrapeData($link);
}
}
public function scrapeData($url)
{
$crawler = GoutteFacade::request('GET', $url);
$title = $this->crawlData('h1.dt-news__title', $crawler);
$description = $this->crawlData('div.dt-news__sapo h2', $crawler);
$description = str_replace('Dân trí', '', $description);
$content = $this->crawlData('div.dt-news__content', $crawler);
$dataPost = [
'title' => $title,
'content' => $content,
'description' => $description
];
Post::create($dataPost);
}
protected function crawlData(string $type, $crawler)
{
$result = $crawler->filter($type)->each(function ($node) {
return $node->text();
});
if(!empty($result)) {
return $result[0];
}
return '';
}
Mình đã chọc vào từng link và xử lý bằng hàm scrapedata, logic của scrapedata, handle thì tương tự nhau và mình cũng đã hướng dẫn từ đầu. Ở cuối mình có lưu vào database.
Cuối cùng bạn chạy lại lệnh:
php artisan scrape:post
Vậy là bạn đã có thể crawler data từ một trang web nào đó rồi. Các bạn sự dụng data với mục định test hoặc nghiên cứu thôi nhé.
Kết luận
Việc crawler data trở nên đơn giản hơn với Goutte. Bạn có thể tham khảo thêm về Goutte tại đây: https://github.com/FriendsOfPHP/Goutte
Nhưng việc sử dụng Goutte vẫn còn nhiều hạn chế. Việc scrape data chỉ dùng lại ở trang đầu. Để xử lý được, cần dùng một browser simulator như puppeteer của Javascript. Nội dung về puppeteer có thể sẽ được thảo luận ở một bài viết trong tương lai.