16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

この記事はエイチーム引越し侍 / エイチームコネクト Advent Calendar 20193日目の記事です。
本日はエイチームコネクト入社4ヶ月目に突入した@ikuma_hayashiが担当します。
SIerから転職して4ヶ月目、まだまだ日は浅く勉強中ですが、Qiita初投稿です!気になる点ありましたらぜひコメントください:bear:

エイチームコネクトって何やってるの?

当社グループのサイトをご利用いただいたユーザー様へ、生活に紐づく必要なサービスのご案内をお電話にて行っております。主に引越しされたお客様に対し、居住場所の変化に伴って生活が豊かになるインターネット回線や電力サービスをご紹介しています。
生活が豊かになるお客様・各種サービスを運営されている会社様・エイチームグループの三方よしを実現するべく、日々追求しております。

なんでやるの?

お客様とオペレーターでされた会話を自然言語処理で解析し、より良いユーザービリティを追求するために、Dockerで言語解析+Webアプリの超基本的な基盤を構築します。

対象の読者

  • Laravelで言語解析したい(文字列から単語を抜き取って重要度を計算したい)
  • Laravel, Dockerは使ったことある

MeCabってなに?

MeCabは 京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです。
中略
ちなみに和布蕪(めかぶ)は, 作者の好物です。

引用元:MeCab: Yet Another Part-of-Speech and Morphological Analyzer

形態素解析とは、文章を形態素という最小単位に分解する技術です。
コマンドラインベースで下記のように形態素解析してくれます。

$ mecab
$ みんなで幸せになれる会社にすること
みんな  名詞,代名詞,一般,*,*,*,みんな,ミンナ,ミンナ
で      助詞,格助詞,一般,*,*,*,で,デ,デ
幸せ    名詞,形容動詞語幹,*,*,*,*,幸せ,シアワセ,シアワセ
に      助詞,副詞化,*,*,*,*,に,ニ,ニ
なれる  動詞,自立,*,*,一段,基本形,なれる,ナレル,ナレル
会社    名詞,一般,*,*,*,*,会社,カイシャ,カイシャ
に      助詞,格助詞,一般,*,*,*,に,ニ,ニ
する    動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
こと    名詞,非自立,一般,*,*,*,こと,コト,コト

MeCabのみではPHPから直接利用できないため、php-mecabも併せて導入し、Laravelから利用できるようにしています。

今回は、これらを用いてLaravel環境で形態素解析を行えるようにします。

環境

  • ホストOS
    • Windows10 Pro
    • Docker for Windows
  • Dockerコンテナ
    • Laravel 6.0
    • php-fpm7.3
    • MySQL 8.0
    • nginx 1.17

ディレクトリ・ファイル構造(一部省略)

laramecab
│  ├─docker-compose.yml
│  └─README.md
├─docker
│  ├─mysql
│  │    └─my.cnf
│  ├─nginx
│  │    └─default.conf
│  └─php
│       ├─Dockerfile
│       └─php.ini
├─logs : 各種ログ・ファイル
└─projects
   └─myapp : Laravelのプロジェクト

(laradockにちなんでlaramecabとかいう大それた名前をつけてみた:bear:

環境構築

コンテナを起動

# 適当なフォルダにてgit clone
PS C:\> git clone https://github.com/IkumaHayashi/laramecab.git
Cloning into 'laramecab'...
remote: Enumerating objects: 18, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 18 (delta 1), reused 14 (delta 0), pack-reused 0
Unpacking objects: 100% (18/18), done.

# 出来たフォルダにてdockerイメージの構築と起動(5分くらいかかります)
PS C:\> cd .\laramecab\
PS C:\laramecab> docker-compose up -d
Creating laramecab_app_1 ... done
Creating laramecab_db_1  ... done
Creating laramecab_web_1 ... done

# 正常に起動しているか確認
PS C:\laramecab> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                                NAMES
ad8e3d23fbc5        mysql:8.0           "docker-entrypoint.s…"   7 seconds ago        Up 4 seconds        33060/tcp, 0.0.0.0:13306->3306/tcp   laramecab_db_1
c6bce5731a4e        nginx:1.17-alpine   "nginx -g 'daemon of…"   About a minute ago   Up 4 seconds        0.0.0.0:8000->80/tcp                 laramecab_web_1
b58a85b0aa2f        laramecab_app       "docker-php-entrypoi…"   About a minute ago   Up 5 seconds        9000/tcp, 0.0.0.0:18000->8000/tcp    laramecab_app_1

MeCabがappコンテナで使えるかどうか確認

# appコンテナのshを起動
PS C:\laramecab> docker-compose exec app sh

# MeCabを起動
$ mecab
今から100年続く会社にすること
      名詞,副詞可能,*,*,*,*,,イマ,イマ
から    助詞,格助詞,一般,*,*,*,から,カラ,カラ
100     名詞,,*,*,*,*,*
      名詞,接尾,助数詞,*,*,*,,ネン,ネン
続く    動詞,自立,*,*,五段・カ行イ音便,基本形,続く,ツヅク,ツズク
会社    名詞,一般,*,*,*,*,会社,カイシャ,カイシャ
      助詞,格助詞,一般,*,*,*,,,
する    動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
こと    名詞,非自立,一般,*,*,*,こと,コト,コト
EOS
# 終了するときはCtrl + Cで抜けられます
^C

Laravelのプロジェクトを作成

# Laravelプロジェクトの作成
PS C:\laramecab> docker-compose exec app composer create-project --prefer-dist laravel/laravel myapp
# いったんdockerコンテナを終了する
PS C:\laramecab> docker-compose stop
Stopping laramecab_db_1  ... done
Stopping laramecab_web_1 ... done
Stopping laramecab_app_1 ... done

nginxの公開フォルダを変更する必要があります。
laramecab\docker\nginx\default.confを開き、下記の通り書き換えてください。

laramecab\docker\nginx\default.conf
server {
    listen 80;
    #root /work/public;
    root /work/myapp/public;
    index index.php;
    charset utf-8;

    location / {
        #root /work/public;
        root /work/myapp/public;
        try_files $uri $uri/ /index.php$is_args$args;
    }
#省略

ふたたびdockerコンテナを起動していきます。

PS C:\laramecab> docker-compose up -d
Starting laramecab_app_1 ... done
Starting laramecab_db_1  ... done
Starting laramecab_web_1 ... done

あとは下記URLを起動すれば、例のLaravelの画面が見えてきます!
http://localhost:8000/
image.png

php-mecabが使えるか確認

laravelにはtinkerというコマンドラインでコードを実行できる機能がありますので、tinkerで確認していきましょう。

# ホストOSからappコンテナでmyappに移動してtinkerを起動する
PS C:\laramecab> docker-compose exec app sh -c "cd myapp/ && php artisan tinker"
Psy Shell v0.9.11 (PHP 7.3.11  cli) by Justin Hileman

# MeCabのインスタンスを生成
>>> $mecab = new \Mecab\Tagger();
=> MeCab\Tagger {#3006}

# 文字列を渡し解析
>>> $nodes = $mecab->parseToNode('今から100年続く会社にすること');

# 解析結果を表示
>>> foreach ($nodes as $n) {
print_r($n->getFeature().PHP_EOL);
}
BOS/EOS,*,*,*,*,*,*,*,*
名詞,副詞可能,*,*,*,*,,イマ,イマ
助詞,格助詞,一般,*,*,*,から,カラ,カラ
名詞,,*,*,*,*,*
名詞,接尾,助数詞,*,*,*,,ネン,ネン
動詞,自立,*,*,五段・カ行イ音便,基本形,続く,ツヅク,ツズク
名詞,一般,*,*,*,*,会社,カイシャ,カイシャ
助詞,格助詞,一般,*,*,*,,,
動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
名詞,非自立,一般,*,*,*,こと,コト,コト
BOS/EOS,*,*,*,*,*,*,*,*

以上で、appコンテナ内でlaravel上でMeCabによる解析ができたことを確認できました!
どうでもいい話ですが、powershellはcdやlsなど、linuxライクなコマンドの利用が可能なので結構おすすめです:star:

せっかくなんでコード書く

ここまでで終わろうと思ってましたがせっかくなんでコードも書いていきます:bear:
下記の流れでアプリを作成していきます。

  1. 解析対象の文字列(Text)、抽出された重要後と重要度(ImportanceTerm)のテーブル、モデル作成
  2. 文字列を渡すとの結果と出現回数をDBに格納するcommandを作成
  3. 重要度を計算し、レコードをupdateするcommandを作成

今回は重要度の計算にtfidf法というものを使っていきます。
tfidf法については、こちらにて概要が解説されていますのでご参照ください。

めちゃくちゃ乱暴に言ってしまうと、

  • その文書内で重要な単語って何回も出るくね?(出現回数tf:Term Frequency)
  • でも一般的な言葉って重要度低いから、レア度が高いほうが重要度高くね?(レア度、固く言うと逆文書頻度idf:Inverse Document Frequency)
  • この2つをかけたら重要度じゃね?(出現回数×レア度=重要度)

というものです:bear:

1. 解析対象の文字列(Text)、抽出された重要後と重要度(ImportanceTerm)のテーブル、モデル作成

今回はTextsテーブル、ImportanceTermsの2つのテーブルを作成していきます。ER図にすると下記のようにTextsとImportanceTermsは1対多の関係になります。
er.png

マイグレーションファイルの作成

appコンテナ
$ php artisan make:migration create_texts_table --create=texts
Created Migration: 2019_12_02_140747_create_texts_table
$ php artisan make:migration create_importance_terms_table --create=importance_terms
Created Migration: 2019_12_02_140908_create_importance_terms_table

マイグレーションファイルの編集

必要なカラムの追加とリレーションを張っていきます。

myapp\database\migrations\2019_12_02_140747_create_texts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTextsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('texts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->longText('text');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('texts');
    }
}

myapp\database\migrations\2019_12_02_140908_create_importance_terms_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateImportanceTermsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('importance_terms', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('text_id')->unsigned();
            $table->foreign('text_id')->references('id')->on('texts');
            $table->string('term', 50);
            $table->integer('frequency')->default(0);
            $table->double('tf', 8, 4)->default(0.0);
            $table->double('idf', 8, 4)->default(0.0);
            $table->double('tfidf', 8, 4)->virtualAs('tf * ( idf + 1 )');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('importance_terms');
    }
}

migrateの実行

appコンテナ
$ artisan migrate
Migration table created successfully.
Migrating: 2019_12_02_140747_create_texts_table
Migrated:  2019_12_02_140747_create_texts_table (0.19 seconds)
Migrating: 2019_12_02_140908_create_importance_terms_table
Migrated:  2019_12_02_140908_create_importance_terms_table (0.48 seconds)

DBコンテナでテーブルの存在を確認していきます。

dbコンテナ
$ # mysql -u root -p
Enter password:
mysql> use default;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
mysql> show tables;
+-------------------+
| Tables_in_default |
+-------------------+
| migrations        |
| importance_terms  |
| texts             |
+-------------------+
3 rows in set (0.00 sec)

モデルの作成

下記の通りコマンドを実行します。

appコンテナ
$ php artisan make:model Text
Model created successfully.
$ php artisan make:model ImportanceTerm
Model created successfully.

モデルファイルが下記の通り作成されますので、Modelsというディレクトリを作成して移動させ、それぞれ編集していきます。

  • myapp\app\Text.php → myapp\app\Models\Text.php
  • myapp\app\ImportanceTerm.php → myapp\app\Models\ImportanceTerm.php

ImportanceTermモデルの編集

先にImportanceTermのほうから。ImportanceTermから見ると、Textは1つしかないので、belogsToを使ってリレーションを張ります。

myapp\app\Models\ImportanceTerm.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ImportanceTerm extends Model
{
    public function text(){
        return $this->belongsTo('App\Models\Text');
    }

}

Textモデルの編集

今度はTextモデルです。Textから見るとImportanceTermsは多なので、hasManyを使ってリレーションを張ります。
ついでに、重要度の降順で並び替えて取得するようにします。
また、textフィールドに設定された文字列からmecabによる解析および単語の出現回数(tf)の計算を行い、一緒に保存するようにします。

myapp\app\Models\Text.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Text extends Model
{
    public function importanceTerms()
    {
        return $this->hasMany('App\Models\ImportanceTerm')->orderByDesc('tfidf');
    }

    public function setImportanceTerms()
    {
        //名詞の抽出と重要度の算出
        $frequencyAndTfs =  $this->generateFrequencyAndTfs();

        //DBに保存
        foreach ($frequencyAndTfs as $key => $frequencyAndTf) {
            $importanceTerm = new \App\Models\ImportanceTerm();
            $importanceTerm->term = $key;
            $importanceTerm->tf = $frequencyAndTf['tf'];
            $importanceTerm->frequency = $frequencyAndTf['frequency'];
            $this->importanceTerms()->save($importanceTerm);
        }

    }

    private function generateFrequencyAndTfs() : array
    {

        //文字列を解析
        $mecab = new \Mecab\Tagger();
        $nodes = $mecab->parseToNode($this->text);

        //形態素ごとに名詞かどうか、重要度はいくつかを算出
        $allTerms = array();
        $terms = array();
        $compoundNoun = '';

        foreach ($nodes as $n) {

            $result = explode(',', $n->getFeature());

            //空白は無視
            if($n->getSurface() == '')
                continue;

            //全単語の頻出回数を記録
            $this->incrementFrequency($allTerms, $n->getSurface());

            //名詞ではない かつ 前も名詞ではない場合はスキップ
            if($compoundNoun == '' && $result[0] != '名詞'){
                continue;

            //名詞ではない かつ 複合名詞が空でない場合は、複合名詞としてカウント
            }else if($compoundNoun != '' && $result[0] != '名詞'){

                //ひらがな1文字は除外する
                if(preg_match('/^[ぁ-ん]$/u', $compoundNoun)){
                    $compoundNoun = '';
                    continue;
                }

                //複合名詞がまだ単名詞の場合は除外する
                if($compoundNoun == $n->getSurface()){
                    $compoundNoun = '';
                    continue;
                }

                //複合名詞を格納
                $this->incrementFrequency($terms, $compoundNoun);
                $compoundNoun = '';

            //名詞 かつ 前の形態素も名詞の場合
            }else if($compoundNoun != '' && $result[0] == '名詞'){

                //前の名詞と複合名詞が一致する場合、前の名詞を単名詞としてカウント
                if($compoundNoun == $n->getPrev()->getSurface()){
                    $this->incrementFrequency($terms, $n->getPrev()->getSurface());
                }

                $this->incrementFrequency($terms, $n->getSurface());
                $compoundNoun .= $n->getSurface();

            //名詞 かつ 最初の出現の場合
            }else{
                $compoundNoun .= $n->getSurface();
            }
        }

        $frequencyAndTfs = array();
        $sumFrequency = array_sum($allTerms);
        foreach ($terms as $term => $value) {
            $frequencyAndTfs[$term] = ['frequency'=> $value
                                       , 'tf' => $value / $sumFrequency];
        }

        return $frequencyAndTfs;
    }

    private function incrementFrequency(&$terms, $term)
    {
        isset($terms[$term]) ? $terms[$term]++ : $terms[$term] = 1;
    }
}

2. 文字列を渡すとの結果と出現回数をDBに格納するcommandを作成

appコンテナでコマンドを作成していきます。詳しい解説は下記を参照してください。
Laravelでコマンドラインアプリケーションを作成する

今回は下記のようにコマンドを打つことで、MeCabによる解析結果をDBに登録し、重要度の計算・更新を行っていきます。

#解析対象の文字列を登録
php artisan TermImportance:store みんなで幸せになれる会社にすること
php artisan TermImportance:store 今から100年続く会社にすること

#idfの計算および重要度の算出
php artisan TermImportance:calc

コマンドの作成

実際にコマンドを作成していきます。

appコンテナ
$ php artisan make:command StoreTextCommand
$ php artisan make:command CalcCommand

そうすると下記の2ファイルができるのでコードを編集していきます。

  • myapp\app\Console\Commands\StoreTextCommand.php
  • myapp\app\Console\Commands\CalcCommand.php

コマンドの編集

StoreTextCommandの編集

myapp\app\Console\Commands\StoreTextCommand.php(一部省略)
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class StoreTextCommand extends Command
{
    protected $signature = 'TermImportance:store {text}';
    protected $description = '計算対象の文字列と各単語の重要度をDBに保管します。';
    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $text = new \App\Models\Text();
        $text->text = $this->argument("text");
        $text->save();
        $text->setImportanceTerms();
    }
}

StoreTextCommandの動作確認

appコンテナ
$ php artisan TermImportance:store みんなで幸せになれる会社にすること
dbコンテナ
$ mysql -u root -p
$ mysql> select * from texts;
+----+-----------------------------------------------------+---------------------+---------------------+
| id | text                                                | created_at          | updated_at          |
+----+-----------------------------------------------------+---------------------+---------------------+
|  1 | みんなで幸せになれる会社にすること                  | 2019-12-02 15:29:16 | 2019-12-02 15:29:16 |
+----+-----------------------------------------------------+---------------------+---------------------+
1 row in set (0.00 sec)
mysql> select * from importance_terms;
+----+---------+-----------+-----------+--------+--------+--------+---------------------+---------------------+
| id | text_id | term      | frequency | tf     | idf    | tfidf  | created_at          | updated_at          |
+----+---------+-----------+-----------+--------+--------+--------+---------------------+---------------------+
|  1 |       1 | みんな    |         1 | 0.1111 | 0.0000 | 0.1111 | 2019-12-02 15:29:16 | 2019-12-02 15:29:16 |
|  2 |       1 | 幸せ      |         1 | 0.1111 | 0.0000 | 0.1111 | 2019-12-02 15:29:16 | 2019-12-02 15:29:16 |
|  3 |       1 | 会社      |         1 | 0.1111 | 0.0000 | 0.1111 | 2019-12-02 15:29:16 | 2019-12-02 15:29:16 |
+----+---------+-----------+-----------+--------+--------+--------+---------------------+---------------------+
3 rows in set (0.01 sec)

よき!当然ですがまだ文章が短くtfは1回だし、idfは計算を実装していないので、デフォルト値0.0になります。

CalcCommandの編集

最後に、idfを計算するロジックを書いていきます。
idfは、全文書のうち該当の単語が存在する文書数を元に計算されます。
(厳密には idf = log10(該当の単語がある文書数/全文書数) )

videoモデルに、重要語有無をチェックするメソッドを実装
myapp\app\Console\Commands\CalcCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class CalcCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'TermImportance:calc';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '各単語のidfを計算します。';

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

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $texts = \App\Models\Text::all();
        foreach ($texts as $text) {

            $importanceTerms = $text->importanceTerms()->get();
            foreach ($importanceTerms as $importanceTerm) {

                $hasTermCount = \App\Models\ImportanceTerm::where('term', $importanceTerm->term)
                                ->distinct()
                                ->count();

                $idf = $hasTermCount > 0 ? log10(count($texts) / $hasTermCount) : log10(0);

                $importanceTerm->idf = $idf;
                $importanceTerm->save();

            }
        }
    }
}

CalcCommandの動作確認

さきほど、「みんなで幸せになれる会社にすること」という言葉を登録しましたが、テストのために下記のように追加でTextの登録と計算を行います。

appコンテナ
$ php artisan TermImportance:store みんなが幸せになれる会社にすることという経営理念では、主体性が生まれないので、みんな”で”に変わった
$ php artisan TermImportance:store 今から100年続く会社にすること
$ php artisan TermImportance:calc

続いて結果の確認を行っていきます。

dbコンテナ
$ mysql -u root -p
mysql> select it.term, it.frequency, it.tf, it.idf, it.tfidf
    -> from importance_terms it
    -> order by it.tfidf desc;
+--------------+-----------+--------+--------+--------+
| term         | frequency | tf     | idf    | tfidf  |
+--------------+-----------+--------+--------+--------+
|            |         1 | 0.1111 | 0.4771 | 0.1641 |
| 100          |         1 | 0.1111 | 0.4771 | 0.1641 |
|            |         1 | 0.1111 | 0.4771 | 0.1641 |
| 100        |         1 | 0.1111 | 0.4771 | 0.1641 |
| みんな       |         1 | 0.1111 | 0.1761 | 0.1307 |
| 幸せ         |         1 | 0.1111 | 0.1761 | 0.1307 |
| 会社         |         1 | 0.1111 | 0.0000 | 0.1111 |
| 会社         |         1 | 0.1111 | 0.0000 | 0.1111 |
| みんな       |         2 | 0.0714 | 0.1761 | 0.0840 |
| こと         |         1 | 0.0357 | 0.4771 | 0.0527 |
| 経営         |         1 | 0.0357 | 0.4771 | 0.0527 |
| 理念         |         1 | 0.0357 | 0.4771 | 0.0527 |
| 経営理念     |         1 | 0.0357 | 0.4771 | 0.0527 |
| 主体性       |         1 | 0.0357 | 0.4771 | 0.0527 |
| 幸せ         |         1 | 0.0357 | 0.1761 | 0.0420 |
| 会社         |         1 | 0.0357 | 0.0000 | 0.0357 |
+--------------+-----------+--------+--------+--------+

母数が少ないのでわかりにくいのですが、

  • 「今」や「100年」という言葉は全体で1回しか出ていない→レア度が高い
  • 「会社」という言葉はすべての文書で出ているためレア度が低い
    という結果となりました。

まとめ

  • Dockerfileでphp-fpmのコンテナにMeCab、ipadic(辞書)、php-mecabをインストールしました
  • DBと連携してtfidf法を用いてテキストから抽出した単語の重要度を計算しました

留意点

  • 本記事ではLaravelとしてどこに処理を記述するか、について主眼を置いてません。
  • 実際はMeCabを使うのであればpythonなどライブラリが充実している言語がおすすめです。PHPと連携したいときは参考になるかと思います。
  • Amazon Lexといった音声から解析できるサービスも出てきており、自然言語処理を実装する必要性がなくなっています。

ソースコード

ソースコードは下記にて公開しております。(個人のGithubアカウントのため会社の活動とは全く関係ありませんのでご了承ください。)

参考記事

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!

Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能です!

明日

明日はいつも助けてくれる @ex_SOULさんの記事です!
魂のこもった記事をお楽しみに!:star2:

16
7
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
16
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?