LoginSignup
2
1

【Laravel】 Goutteとmecabで形態素解析やってみた

Last updated at Posted at 2024-03-31

 はじめに

「形態素解析」って?

  • 文章を言葉が意味を持つまとまりの単語の最小単位(形態素)にまで分割する技術のこと。
    例えば、「私は台所で料理します」という文章を形態素解析すると
    • 私(代名詞)
    • は(副助詞)
    • 台所(名詞)
    • で(助詞)
    • 料理(名詞)
    • し(動詞)
    • ます(助動詞)
      という具合に分けられます。

  • 形態素解析が使えるライブラリは
    • mecab(C)
    • Janome(python)
    • sudachi(Java)

などがあります。
今回はmecabを使ってみます!

やってみよう

開発環境  

  • MAMP
  • PHP7.4.21
  • Laravel 8
  • MySQL

ライブラリなど

  • mecab : 形態素解析ライブラリ
  • ateliee/mecab : mecabをPHPで使えるようにするライブラリ
  • POS tagger : mecabだと、英語単語の品詞が取得できないので英単語はこのライブラリを使って判断
  • Laravel Goutte :Laravelのスクレイピングライブラリ
  • スクレイピングするサイト:Uta-Netのアーティストページを今回は使用

ざっくり処理の流れ

  1. Goutteを使ってスクレイピングするサイトから歌詞を取得
  2. 取得した歌詞をDBへINSERT
  3. 2の結果をmecabを使って解析し、DBへINSERT

DBイメージ

こんな感じ:
スクリーンショット 2024-03-31 18.36.31.png

つくってみる

ブラウザで動かすのもなんだかなーと思ったので、Batchにして見ました。
今回はスクレイピングするBatchと形態素解析するBatchに分けてみました。

app\Console\Commands\insertLyrics.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Weidner\Goutte\GoutteFacade as Goutte;
use App\Models\Songs;
use App\Models\Artists;

class insertLyrics extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'insert_lyrics {artistID}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'insert Lyrics!';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $artistID = $this->argument('artistID');
        $exist_songs = Songs::where('artist_id', '=', $artistID)->get();

        if (count($exist_songs) > 0){
            Songs::where('artist_id', '=', $artistID)->delete();
        }

        $artist_url = "https://www.uta-net.com/artist/{$artistID}/";
        $songs = [];
        //URL
        $crawler = Goutte::request('GET', $artist_url);
        $urls[] = $crawler->filter('a.py-2.py-lg-0')->each(function ($node) {
            return $node->attr('href');  
        });

        //title
        $titles[] = $crawler->filter('.songlist-title')->each(function ($node) {
            return $node->text();
        });

        for ($i = 0; $i < count($urls[0]); $i++){
            $crawler = Goutte::request('GET', 'https://www.uta-net.com/'.$urls[0][$i]);
            $lyric = $crawler->filter('#kashi-box')->each(function ($node) {
                return $node->text();
            });
            $song = new \App\Models\Songs;
            $song->fill([
                'artist_id' => $artistID,
                'title' => $titles[0][$i],
                'lyric_url' => 'https://www.uta-net.com'.$urls[0][$i],
                'lyric' => $lyric[0],
            ])->save();
        }

        $update_songs = Songs::where('artist_id', '=', $artistID)->get();

        //不要な文言を消す
        foreach ($update_songs as $update_song) {
            $update_song->lyric = preg_replace('/ この歌詞をマイ歌ネットに登録 >このアーティストをマイ歌ネットに登録 >$/', '', $update_song->lyric);
            $update_song->save();
        }

        echo("finished!");
        return 0;
    }
}

形態素解析する方

app\Console\Commands\analyzeLyrics.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\Songs;
use App\Models\Artists;
use App\Models\AnalyzedLyrics;
use meCab\meCab;
use StanfordNLP\POSTagger;

class analyzeLyrics extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'analyze_lyrics {artistID}';

    /**
     * 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()
    {
        $artistID = $this->argument('artistID');
        $mecab = new meCab();
        $modelPath = storage_path('app/stanford-postagger/models/english-left3words-distsim.tagger');
        $jarPath = storage_path('app/stanford-postagger/stanford-postagger-4.2.0.jar');
        $pos = new POSTagger($modelPath, $jarPath);

        $songs = Songs::where('artist_id', $artistID)->get();
        AnalyzedLyrics::where('artist_id', $artistID)->delete();

        foreach ($songs as $song){

            $songID = $song->id;
            $lyric = $song->lyric;
            $pattern = '/\b[a-zA-Z]+\b/';
            $japaneseLyric = preg_replace($pattern, '', $lyric);
            $englishLyric = preg_replace('/[^a-zA-Z\s\']/', ' ', $lyric);

            $japaneseAnalyzedLyrics = $mecab->analysis($japaneseLyric);
            foreach ($japaneseAnalyzedLyrics as $nodes){
                $text = trim($nodes->getText());

                if (!preg_match('/[\p{P}\p{S}]/u', $text)){
                        $speech = $nodes->getSpeech();                    

                    $analyzedLyrics = new AnalyzedLyrics;
                    $analyzedLyrics->fill([
                        'artist_id' => $artistID,
                        'song_id' => $songID,
                        'pos' => $speech,
                        'text' => $text,
                    ])->save();
                }
            }

            $englishAnalyzedLyrics = $pos->tag(explode(' ', $englishLyric));
            foreach ($englishAnalyzedLyrics as $node1){
                foreach ($node1 as $node2){
                    $speech = $node2[1];
                    $text = $node2[0];

                    $analyzedLyrics = new AnalyzedLyrics;
                    $analyzedLyrics->fill([
                        'artist_id' => $artistID,
                        'song_id' => $songID,
                        'pos' => $speech,
                        'text' => $text,
                    ])->save();
                }
            }


        }
        echo("finished!");

        return 0;
    }

    public function getPOSofNotJapanese($text)
    {
        $modelPath = storage_path('app/stanford-postagger/models/english-left3words-distsim.tagger');
        $jarPath = storage_path('app/stanford-postagger/stanford-postagger-4.2.0.jar');
        $pos = new POSTagger($modelPath, $jarPath);
        $result = '';

        $analyzedWord = $pos->tag(explode(' ', $text));
        foreach ($analyzedWord as $node1){
            foreach ($node1 as $node2){
                $result = $node2[1];
            }
        }
        return($result);
    }

}

これを実行するとDBはこんな感じ

  • Songs(スクレイピングしてINSERTしたデータ)
    スクリーンショット 2024-03-31 18.48.24.png

  • analyzed_lyrics(形態素解析した結果)スクリーンショット 2024-03-31 18.50.48.png

まとめなど

  • これを使ってwordcloudを作るぞ!と言うか作ったのですが、MAMPがいきなり立ち上がらなくなると言う不具合が起きてしまい、再インストールする羽目に・・・いろいろ設定終わったら続編書きます。
  • POS taggerはあくまでも英単語を品詞分けするのであって、例えばドイツ語が来た時は対応してくれないのでどうにかならないかなと考えています。良い方法ないかなー。
  • これを作ろうと思ったきっかけの記事はこちら:ミスチルの歌詞をJanomeで解析してWordCloudで可視化してみた

最後までお読みくださってありがとうございました!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1