技術的な話題 に関する投稿を表示しています

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる MC-value について理解した(つもり)

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

FLR の件と同じ論文で出ていた MC-value について。「単名詞バイグラムによらない用語スコア付け」として挙げられている

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ | ごみばこいん Blog

元の論文

FLR と同様に「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られているのでスキップ。
そして MC-value を語る前に、その元になる C-value を語らないと行けない。

C-value のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 連続する名詞って入れ子になることあるよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

C-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 複合名詞
    1. 例) トライグラム 統計
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム 統計) = 2
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム 統計) = 2
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム 統計) = 1
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム 統計) = 1
  6. C-value(CN) = (length(CN) - 1) * (n(CN) - (t(CN) / c(CN)))
    1. 例) C-value(トライグラム 統計) = (2 - 1) * (2 - (1 / 1)) = 1

このとき length(CN) = 1 、つまり単名詞のときに数値が 0 になってしまうという問題がある。専門用語は単名詞になることもあるだろう。

MC-value のものすごいざっくりした理解

  • C-value で連続する名詞のスコア計算が出来たけど単名詞…
  • -1 してたものをなくせばいいじゃん!

MC-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 名詞
    1. 例) トライグラム
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム) = 1
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム) = 10
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム) = 7
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム) = 5
  6. MC-value(CN) = length(CN) * (n(CN) - (t(CN) / c(CN)))
    1. 例) MC-value(トライグラム) = 1 * (10 - (7 / 5)) = 8.6

論文上では 5.6 と書かれているけどこの数字がどうやって出てきたかわからんかった…。
n(CN) の計算がもしかしたら違うかも。

例1(図2)の場合,MC-value(トライグラム)=(7−7/5) = 5.6である

まとめと感想

そもそも C-value が TF-IDF や FLR と異なり、全ての文字列に対して下準備をする必要がないのと、数えることがメインなので計算量すくなく、データ量をどんどん増やすようなことをしてもお手軽に使えそうな気がする。

論文中でもいい感じに取れるぜ!的なことが書いてあるので、もうちょっと実践的に?入れて様子を見ようかなあと思いましたとさ。

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

以前 TF-IDF について調べた。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた | ごみばこいん Blog

まったく違うアプローチをしている重要語を抽出するようなものがないかなーと調べてたら FLR というものに行き着いた。

元の論文

「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られている。

機関リポジトリ内のページ
UTokyo Repository - 東京大学学術機関リポジトリ

関連サイト
専門用語(キーワード)自動抽出システム”のページ

ここで実際どうなるのよ、が試せるっぽいのと、ライブラリが配布されているので組み込むのもまあまあ容易に行けそう。

FLR のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

FLR の仕組み

  1. 単名詞バイグラム、LNnとRNnのリストを作る
    1. [LNn N](#Ln)
    2. [N RNn](#Rn)
    3. 例)トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム
      1. LNn
        1. [単語 トライグラム](3)
        2. [クラス トライグラム](1)
        3. [文字 トライグラム](1)
      2. RNn
        1. [トライグラム 統計](2)
        2. [トライグラム 抽出](1)
  2. 単名詞スコア1 として、連接種類数 LDN(N)とRDN(N) を作る
    1. 種類の数なので、単語の出現頻度にはさほど影響されない
    2. 例)
      1. LDN(トライグラム) = 3
      2. RDN(トライグラム) = 2
  3. 単名詞スコア2 として、連接頻度 LN(N)とRN(N)を作る
    1. 単名詞バイグラムを特徴付けるものとして、全ての出現回数の合計を取る
    2. 例)
      1. LN(トライグラム) = 3 + 1 + 1 = 5
      2. RN(トライグラム) = 2 + 1 = 3
  4. 複合名詞スコア LR(CN) を作る
    1. 専門用語は複合名詞が多いが、その複合名詞の長さによって重要度はかわらない
    2. 単名詞Nに対し、左側スコアFL(N)、右側スコアFR(N)
      1. 単名詞スコア1 か 2 を利用する
      2. 複合名詞 CN = N1, N2, ... NL
      3. 複合名詞に含まれる各単名詞について FL, FR を計算し、相乗平均を取り、CNの長さLの逆数のべき乗を取ったものが LR(CN)
      4. LR(CN) = pow( GeometricMean( (FL(Ni) + 1) * (FR(Ni) + i) ) , 1/2L)
      5. 例)連接頻度をスコアとすると
      6. LR(トライグラム) = pow( (5+1) * (3+1), 1/2 ) = sqrt(6 * 4) ~= 4.90
  5. 出現回数を複合名詞スコアに加える FLR(CN)
    1. f(CN) = CNが単独で出現した頻度、他の複合名詞に含まれていないこと
    2. FLR(CN) = f(CN) * LR(CN)
    3. 例)連接頻度をスコアとすると
      1. FLR(トライグラム) = 3 * 4.90 = 14.70

まとめと感想

名詞に対して、よくでる連続した名詞がスコア高くなるような仕組みでスコアを付けて、それをピックアップできるものだった。
何かに特化した専門的なものを説明するようなコンテンツだとその節があると感じる。(例えばこの記事だと "名詞" は上位にピックアップされるんじゃないかな)

というか、最初に重要語が~みたいな話を書いたけど、「重要とは何か?」という考え方、指標、スコアを設ければそれで重要ってキメになるんだろうな。

  • 出現回数「よく出る単語は重要だ!」
  • TF-IDF「このドキュメントだけよく出るから重要だ!」
  • FLR「この名詞はよくくっついて出るから重要だ!」

ちなみにこの論文では MC-value というものも提案されており、それはまた書く。

PHP で本文抽出したいよね、という

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

前回の記事: ExtractContent を PHP で書き換えた | ごみばこいん Blog

「今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい」といいつつ、他のネタが挟まり、ようやくにして比較しました。

比較した結論

お試しにはこちらのリポジトリをどうぞ。
sters/compare-article-extractors: Compare web article extractors.

日本語環境下で難しい話

php-goose 、または php-web-article-extractor では、どちらも内部にストップワード辞書を持っていて、プログラム上で言語を指定、あるいは lang 属性や meta タグから読み取った言語名から、適切なストップワード辞書を選択しています。そうして決まったストップワードの一覧が、文字列にどれくらいあるかを確認して、本文かどうか?といった判断を進めているようです。

ストップワードとはよく使われがちな単語のことで、自然言語処理の前処理として行なうことでデータ量を減らしたり、精度を上げたり出来ます。

自然言語処理における前処理の種類とその威力 - Qiita

例えば英語では「This is a pen.」といったようにスペースで区切られているので「ストップワードの is と a の 2 文字がある!こいつはコンテンツだ!」と簡単に出来るのですが、日本語の場合はそう簡単な話ではありません。日本語はスペース区切りではありませんし、前後にある文字によって、まったく異なる意味合いになったりもしますので、一概に辞書でドバーッと指定することは適切な処理でない可能性が高いです。

じゃあどうするかというと分かち書きの出番なのですが、分かち書きも簡単なものではありません。 Chasen や Mecab といった既存の技術を利用すればお手軽にもできますが php-goose や php-web-article-extractor がそれを利用して日本語対応するかというとちょっと違いそうだなあ、という気持ちです。

php-goose に出したプルリクは諦めてドバーッと処理する形ではありますが、日本語だけ特殊化せざるを得ないので、そうやってみて出してみたものの、なんだかなあと。いやそもそも 元になってる Goose はそういう実装になっていないだろうし…。

そのほかのアプローチ方法

DOM構造や、画面上の位置情報も利用するのはめっちゃ有効的だと思います。
PuppeteerでWebページからメインコンテンツっぽいところを抽出してみる - Qiita

上記の記事の「まとめ」でも出てはいますが、機械学習して取るようにするのもよさそうです。
seomoz/dragnet: Just the facts -- web page content extraction

WordPress と Twitter を連携するテスト

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

テストだよーん。

====

追記してみるよ。

今まで Simple Tweet を利用していたのですが、Twitter アプリの見直しをして、うっかり全部消してしまい、再度アプリを作成しても使えなくなっていました。
PIN コードの入力…、PIN どこ… と、いうような状況です。

Simple Tweet — WordPress Plugins

調べたところ WP to Twitter がよさそうだったので、これを入れました。

WP to Twitter — WordPress プラグイン

設定画面が日本語化されていますし、流れで設定していけるので簡単に利用できます。

DynamoDBを調べたメモ

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

DynamoDB 使ったほうがいいんじゃない!?!?という話になったので、調べたメモ。まじでメモ。

+ 金額感
    + ストレージ利用料 + キャパシティユニットと呼ばれる処理単位 + リクエスト数
    + キャパシティユニット
        + 書き込みの制約
            + 1つの書き込みキャパシティユニットで秒間1回書き込める
            + 1つのキャパシティユニットで1KBまで
        + 読み込みの制約
            + 1つの読み込みキャパシティユニットで秒間2回読み込める
            + 1つのキャパシティユニットで4KBまで
        + これを消費する量をスループットと呼んでいる
            + Dynamoを使っている上でスループットが上がらない、はキャパシティユニットを上げればいい
        + 読み書きそれぞれ別の値を設定できる
        + オートスケールも可能。70%をしきい値に増やしたり減らしたりできる
        + この制約があるので一括でドッバー!みたいなことができない
    + キャパシティユニットの課金が厳しいのでがっつりDBストアというよりは、無限にスケールできるKVSのイメージ
+ ストレージ
    + 縦にも横にも自動でめっちゃ伸びる
    + 実態はSSD。ソフトウェア的なものはAWS独自?まあ気にしなくて良い領域
+ インデックスの設定が必要
    + なぜ必要?
        + 無限にスケールが出来る、一方でどうやってデータを検索するの?
        + 高速に検索するために、データに対して、一意に絞りこめるようなキーを設定してあげる必要がある
    + どんなインデックスがある?
        + プライマリ
            + HashKey or HashKey + RangeKey
            + 一意に絞りこめるキーと値の幅のインデックスが設定できる
        + LSI
            + ローカルなセカンダリインデックス
            + HashKey + RangeKey のときだけ使える
            + 別のRangeKeyを増やすイメージ
        + GSI
            + グローバルなセカンダリインデックス
            + プライマリキーはそれとして、別のインデックスを設定できる
        + セカンダリインデックスを設定するときのオプションで金額が変わる
            + 「射影しなかった属性データはインデックスでのクエリ結果に含まれない」っぽい
                + もし射影してない属性データが欲しい場合は改めてテーブル側から取得する必要があり
                + その分余計なユニットを消費してしまうので逆にコストが高くなることも
    + HasKeyは分散させないとスループットが出ない
        + ストレージがいい感じに分散されるため
        + 単に日付をべべっとしたデータを入れると分散されない
+ 整合性
    + 2つある
    + つよい
        + つよい。ちゃんと整合性を見てくれる
        + スループットを倍消費する
    + ふつう
        + ふつう。書き込みは同期じゃなくてちょい遅延がある
        + スループットは通常(1キャパシティユニットで秒間2回読み込める)
+ データバックアップ
    + 勝手に分散保存されるので不要(S3といっしょ)
    + バックアップしようとするとキャパシティユニットがつらい
+ RDS, Dynamo, S3 を比べてみる
    + RDS
        + スケール
            + スケールアップ、縦横分割は自分でやる
        + スピード
            + インデックスが効けば早い
            + データ量が増えるとつらい
                + 日ごとにめちゃめちゃ増えるデータに対しては難しい
            + Auroraで多少改善できるかも?
        + 検索
            + わりかし柔軟に検索できる
        + 一貫性
            + ◎トランザクション
        + その他
            + 要するにMySQLなので理解が容易
    + Dynamo
        + スケール
            + 無限にスケール出来る、縦横分割を考えなくていい
        + スピード
            + 強制的にインデックスが効くので早い
            + DAXというオプションで加速できる
        + 検索
            + インデックスに基づく簡単な検索は出来る
            + テーブルをくっつけるような複雑目なものは無理
                + ElasticSearchと連携して最高の検索ソリューションを提供できる
        + 一貫性
            + トランザクションはない
        + その他
            + キャパシティユニットを超えるとエラーになるのでスパイク対応が難しい?
                + オートスケール+ライブラリがリトライ対応してると最高
    + S3
        + スケール
            + 無限にスケール
        + スピード
            + 他と比べると遅め
            + おそすぎてやばい!というほどではなく、耐えられるくらいの遅さ
        + 検索
            + 検索することはできない、一意に絞りこめるキーを決める必要がある
            + ElasticSearchにまるっと放り込んで検索することもできるっちゃ出来る
        + 一貫性
            + トランザクションはない
        + その他
            + 要するにファイルストレージ
            + 保存コストは一番安いが、取り出しコストは大きめ?
+ 余談
    + GoogleAnalyticsはBigTableがバックエンド

勉強した色々な記事

仕事スクレイピングを楽にした

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

この記事は Webスクレイピング Advent Calendar 2017 - Adventar の 5 日目の記事です。

お仕事都合による発生するスクレイピングについてと、それつらたん~~~なことになってきたので解消するようにブチアゲした話を書きます。

お仕事都合による発生するスクレイピング

お仕事都合、例えば営業や企画といった人達から「こういうこと出来ないかなあ…?」とかって話が来るとおよそ7,8割の確立でスクレイピングできないっすかというお願いです。それが 1 回だけならいいんですが、異なる人から、異なることを、ちょこちょことお願いが来たりします。
さらに厄介なのが「XX さんに先月お願いしたやつ、今月もよろしく」みたいなやつ。ぼくは XX さんじゃないし、その事があったことを知らないぞ??

そしてその "お願い" の中身としては、例えばこういう感じ。

  • 指定する URL 群から XXX と YYY の情報を集めたい
    • (これがいちばんおおい)
  • 電車案内のサイトから駅名一覧を集めたい
  • 求人系のサイトから職種一覧、募集企業の情報を集めたい
  • IRまとめ系サイトから業種や会社名、連絡先を集めたい
  • 特定のメディアサイトに入っている画像を集めたい
  • 〇〇のメーカーの商品、型番の一覧を集めたい
  • ムフフなコンテンツ集めたい
  • etc...

そしてこういったお願いはかなりの速度感を求められることがあります。

とはいえ、どばーーーーってスクレイピングしたら、対象のサイトに対して、迷惑を掛けることにつながりますし、訴訟リスクも存在します。前提として 1 日目の vaaaaanquish さんが書かれているようなこと( PythonでWebスクレイピングする際の規約回りの読み込み - Stimulator ) を
確認しつつ、良心的な範囲でやらなければいけないので、その点については説明し納得をしてもらっています。

そんなこんなで、これまではまあいっかーという気持ちで、毎回 0 からコードを書いて、ぐるぐるしたりしてやっているのですが、いやいやちょっとそろそろ面倒になってきましたよ、ということでもうちょっとやることにしました。

「コレ使えばおおよそいいよ」セットを作る

毎回コードを書くのはまあいいとして、でも毎回 0 スタートはかなり面倒です。ボイラープレートというか、フレームワークというか、何かそういう「コレ使えばおおよそいいよ」セットを作ります。
このセットがあることによって、コードがざっくりと統一されるので、引き継ぎだったり、並行開発だったり(するか?)、そのあたりの勝手が良くなります(なりました)。

Guzzel 使えばいいじゃん、とかそういう話でもあるんですが、スクレイピングしたときってデータの入出力はもちろん、スクレイピングのやり方によってはキューイングしたり、一時データをもったりするじゃないですか。そういうところ全部扱いたいんですよ。で、後述するんですけど、速度感よくやっていこうとすると、あーだこーだ composer require するってよりは、ファイル 1 個読めばいいじゃん、という環境にしたいのです。

制約とか

お仕事の都合によって、言語の制約は PHP になります。他の言語でもいいのですが、周りの人の取り回しを考えるとこれがベストマッチです。

また composer や PEAR は利用できません。お願いを解決するにあたって速度感よくやるために「コレ使えばおおよそいいよ」セットは、何も考えずにとにかくサクッと使えることが必要です。なんなら PHP の設定も気にせずに使えるように、どこかのサーバにファイルを設置して eval(file_get_contents()) みたいに使えると最高です。

そうして出来たものがこちらです

easy scraping kit
※ CSS セレクタを扱う箇所がざっくり過ぎて正しくないです

コレを使って、例えば「Mapion を利用して、指定する都道府県・市区町村の駅名を集める」ものを書くとこんな用になります。

// プログラム本体
eval(file_get_contents('https://.../scraping_kit.txt'));

HTMLDoc::$waitMin = 5000;
HTMLDoc::$waitMax = 15000;

$baseUrl = 'https://www.mapion.co.jp';

foreach(Console::getListFile('list.csv') as $zip) {
	Console::out("start {$zip[0]}");

	// search pref
	$topDoc = HTMLDoc::loadURL($baseUrl . '/station/');
	$prefLinks = $topDoc->findCSSPath('.section.type-a a');
	foreach($prefLinks as $prefLink) {
		if ($prefLink->textContent !== $zip[0]) {
			continue;
		}
		Console::out("found {$zip[0]}");
		Console::out("start {$zip[1]}");

		// search city
		$prefDoc = HTMLDoc::loadURL($baseUrl . $prefLink->attributes['href']->value);
		$cityLinks = $prefDoc->findCSSPath('.section.type-a a');
		foreach($cityLinks as $cityLink) {
			if (mb_strpos($cityLink->textContent, $zip[1]) === false) {
				continue;
			}

			Console::out("found {$zip[1]}");

			// search station
			$cityDoc = HTMLDoc::loadURL($baseUrl . $cityLink->attributes['href']->value);
			$h1 = $cityDoc->findCSSPath('h1.type-a-ttl');
			$h1 = explode('の', $h1[0]->textContent);
			$h1 = $h1[0];

			$trainLinks = $cityDoc->findCSSPath('table.list-table tr');
			foreach($trainLinks as $train) {
				$item = [
					$h1,
				];
				$tds = $cityDoc->findCSSPath('td', $train);
				foreach($tds as $td) {
					$item[] = str_replace('[MAP]', '', trim($td->textContent));
				}
				Console::outputResult($item);
			}
		}

		Console::out("done {$zip[0]}{$zip[1]}");
	}
}
// list.csv の例
神奈川県,川崎市
神奈川県,横浜市

今ままで 0 から書いて、1,2 時間かかってコードを書いていたのがサクッと終わるようになりました。
また、簡単にではありますが、キューやキャッシュも持てるようになるので、 cron などを使って定期的に回したり、 web サイト A の情報を元に web サイト B の情報を取得するといった複数のステップがあるようなスクレイピングもわりかし簡単にできるようになりました。

ごく簡単なスクレイピングはコードを書かずに済ませたい

「コレ使えばおおよそいいよ」が出来ましたが、そもそもとして、よく発生するお願いは「指定する URL から XXX と YYY の情報を集めたい」です。これってコードを書く必要はほとんどないはずで、入力として URL と CSS セレクタ( もしくは XPath セレクタ)さえあれば、あとはよろしく処理するような仕組みがあれば十分なはずです。

というわけで一旦雑に作って様子を見ています

11 月くらいに作って様子を見ようと思っていたのですが、特に何かあったわけでもないんですが、急にお願いが減っちゃって作らなくていいかなあと思ってきました。とはいえ怠けているとあとが大変になりそうな予感がするので、近いうちにやりたい気持ち。

おわりに

「コレ使えばおおよそいいよ」を作ってお仕事都合のスクレイピングを簡単にしたり、別の開発者が入るときも問題の起きにくい状況になりました。
しかし、もはやこの件についてコードを書くことをなるべくやめたいので、そういう「スクレイピングいい感じにできるツール」みたいなのあったら教えてください。

(Google スプレッドシートで出来ることは知っていますが、時間系のあたりをもうちょっと自由度が効く感じで…)


6 日目は _kjou さんで「何か書きたい」です。こちらもお楽しみに!

Webスクレイピング Advent Calendar 2017 - Adventar

ExtractContent を PHP で書き換えた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

いや Ruby でええやん、 Python でええやん、みたいな話だとは思うのだが、やっぱり PHP でやりたいよね、という一定の需要がギョームで発生してしまったので、 Ruby のコードを見ながら PHP に書き換えた。 Packagist にも登録してあるので composer からどうぞ。

sters/extract-content - Packagist

$url = 'http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html';
$extractor = new \ExtractContent\ExtractContent(file_get_contents($url));
$result = $extractor->analyse();
file_put_contents(__DIR__ . '/result', $result);

// // 抽出結果
// Webページの自動カテゴライズ の続き。
// 前回書いたとおり、パストラックで行っている Web ページのカテゴライズでは、Web ページの本文抽出がひとつの鍵になっています。今回はその本文抽出モジュールを公開しつつ、使っている技法をざっくり解説などしてみます。
// 本モジュールの利用は至極簡単。require して analyse メソッドに解析したい html を与えるだけ。文字コードは UTF-8 です。
// ...

ExtractContent は Cybozu の Nakatani Shuyo さんが2009年に作成したもので、正規表現を主として Web 記事上の本文に関するちょっとの知見が加わることで成り立っている。以下の記事へ。

Webページの本文抽出 (nakatani @ cybozu labs)

今回、移す際に参考にしたのは、もともとのものではなく、Ruby 1.9 に対応したソース。というのもこっちの記事を見つけたのは後で、 Github 上で ExtractContent を先に見つけていたので、まあいっか、と。

mono0x/extractcontent: ExtractContent for Ruby 1.9+

Ruby はチョットヨメルので inject とかわからないメソッドだけドキュメント見つつ脳内補完して、同じような感じの処理になるよう PHP へ書き直した。工夫したところはとくになく、クラスで扱うようにした、チョットテスト書いたくらいで、ほぼそのまま移してきた。Wikipedia と Medium、はてなブログあたりで試してみたところで、おおよそうまくいっているように見えたので、たぶん大丈夫。

ただ、試していて、本文っぽいと判断されるのが 2 つ以上あるような 1 記事ページ(例えば記事中に section タグがあってーとか、そいういう)ではうまく取り出せず、スコアが高くなったほうのみ抽出されてしまう。本文しきい値のようなものを設けてそれを超えていたら、結合して出す、とかしないといけないなあと思う。とはいえ、正確に本文が欲しいのか、その記事中の重要な部分にフォーカスするのか、などなど要件にもよるので、とりあえずはいいんじゃないかなの気持ち。

ちなみに PHP による実装もいたのだが、おそらく上記のようなところで、元々のものには無いオプションが増えていたりでチョットわからなかったので、一から書いた次第。

aoiaoi/ExtractContent: extract content from HTML

なお記事解析、本文抽出について Packagist を調べると他にも 3 つのライブラリが出てくる。ざっくり紹介するとこんな感じ。

今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい。

faker を使ってダミーデータを生成する

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

これこれ、このライブラリ。

fzaninotto/Faker: Faker is a PHP library that generates fake data for you

ざっくり翻訳しつつ、日本語データでの使い方へ。

faker とは

faker は PHP のライブラリで、偽のデータを提供する。データベースの初期化や XML ドキュメントの生成、ストレステスト、本番データの匿名化、などに活用できる。faker は Perl の Data::Faker と Ruby の Faker に影響を受けている。

faker の使い方

インストール方法

Pakagist にて提供されているので composer を使ってインストールできる。

composer require fzaninotto/faker

基本的な使い方

$faker = \Faker\Factory::create();
// faker オブジェクトの生成

echo $faker->name;
// 名前。例えば 'Lucy Cechtelar'

echo $faker->address;
// 住所。例えば
// "426 Jordy Lodge
// Cartwrightshire, SC 88120-6700"

echo $faker->text;
// 文章…というよりは Lorem 。例えば。
// Dolores sit sint laboriosam dolorem culpa et autem. Beatae nam sunt fugit
// et sit et mollitia sed.
// Fuga deserunt tempora facere magni omnis. Omnis quia temporibus laudantium
// sit minima sint.

faker には嬉しいことに、日本語を含む色々な言語のダミーデータをサポートしている。ここでは日本語での扱いを試してみる。

// 引数に言語を指定すると利用してくれる
$faker = \Faker\Factory::create('ja_JP');

echo $faker->name;
// 例えば '大垣 直子'

echo $faker->address;
// 例えば '2636573  滋賀県浜田市西区笹田町渡辺1-1-10'

echo $faker->realText;
// text は Lorem なので realText がある
// 例えば 'オン燈と、もうすっかりがきの音にすきっと思いなやさで伝つたわ」「なんだ」カムパネルラはこち見ていたの...'

どういうプロパティが用意されているか

英語プロパティは README に全てあって、n - m の範囲で乱数とか、「Dr.」などの敬称を使えるとか、日時とか、まあなんか色々とあるのでそっちを見ると良さそう。

Faker/readme.md at master · fzaninotto/Faker

日本語のだけ、ソースとにらめっこしないと分からないところがあったので、取り上げる。(網羅できてないかも)

country 国名
prefecture 都道府県
city
ward
streetAddress 町以下
postcode 郵便番号
secondaryAddress マンション名
company 会社名
userName ユーザ名
domainName ドメイン名
email メールアドレス
name 名前(姓 + 名)
lastName
firstName
firstNameMale 名(男性)
firstNameFeMale 名(女性)
kanaName 名前カナ
lastKanaName 姓カナ
firstKanaName 名カナ
firstKanaNameMale 名カナ(男性)
firstKanaNameFemale 名カナ(女性)
phoneNumber 電話番号
realText 日本語文章

ORMとの連携

faker は CakePHP や Laravel の ORM と連携することができる(!?)
試してみたところ、連携というよりは ORM を通して、実際に DB に値を格納してくれるっぽい。

例えば CakePHP3 で使うならこんなふうに。

$faker = \Faker\Factory::create('ja_JP');
$populator = new \Faker\ORM\CakePHP\Populator($faker);
$populator->addEntity('Users', 5, [
    'name'       => function() use ($faker) { return $faker->name; },
    'prefecture' => function() use ($faker) { return $faker->prefecture; },
    'created'    => null,
    'modified'   => null,
]);
$inserted = $populator->execute();

$Users = \Cake\ORM\TableRegistry::get('Users');
debug($Users->find('all')->toList());
// 例としてこのような出力になる
// [
//     (int) 0 => object(App\Model\Entity\User) {
//         'id' => (int) 1,
//         'name' => '津田 裕樹',
//         'prefecture' => '栃木県',
//         ....
//         'created' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//         'modified' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//     },
//     ...
// ] 

特に指定をしなかったカラムについては DB の形を見て、結構いい感じに適当に faker な値で埋めてくれる。指定はクロージャで、 faker を使ってランダムなデータを作ることも出来るし、固定値にすることもできる。また、例としては 1 エンティティしか作らなかったが、addEntity を複数呼んで、 まとめて execute することで複数のエンティティも作ることができる。

シード値を設定する

テストで使うときやっぱりランダムだとつらいよね、というときのためにシード値を設定することができる。

$faker = \Faker\Factory::create();
$faker->seed(1234);

echo $faker->name;
// 何度コードを流しても以下の順番で流れる
// 'Miss Lorna Dibbert'
// 'Litzy Emard'
// 'Odessa Collins'
// ...

ランダムに提供される部分はそのとおりなのだが、日付については引数を指定しないと now() な値が使われるのでシードは同じでも出力される値が変わってしまうので、なんでもいいが毎回同じ値を入れる必要がある。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime();
// ランダムになる

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime('2017/01/01 12:00:00');
// 固定パターンになる

faker には乱数を提供する関数もあり、シード値を設定することで、こういったものたちも固定パターン化される。便利ちゃん。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->numberBetween(0,100);
// 固定パターンになる
// 81, 50, 62, 18, 56, ...

faker の内部、プロバイダの話

何の気無しに \Faker\Factory::create() を呼び出していたが、実は内部でこんなような処理をしている。

$faker = new \Faker\Generator();
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
$faker->addProvider(new \Faker\Provider\en_US\Address($faker));
$faker->addProvider(new \Faker\Provider\en_US\PhoneNumber($faker));
$faker->addProvider(new \Faker\Provider\en_US\Company($faker));
$faker->addProvider(new \Faker\Provider\Lorem($faker));
$faker->addProvider(new \Faker\Provider\Internet($faker));

※実際のコードはこのあたり。
Faker/Factory.php at master · fzaninotto/Faker

プロバイダの土台として \Faker\Provider\Base がいるので、これを継承した適当なクラスを作り、 public なメソッドを作って return すれば良い。そのプロバイダクラスを new して addProvider すれば使えるようになる。

class Book extends \Faker\Provider\Base
{
  public function title($nbWords = 5)
  {
    $sentence = $this->generator->sentence($nbWords);
    return substr($sentence, 0, strlen($sentence) - 1);
  }

  public function ISBN()
  {
    return $this->generator->ean13();
  }
}


$faker->addProvider(new Book($faker));

echo $faker->ISBN;

faker を使ってみて

説明にもあるが、テストコードにおいて、いや、そこはなんでも良いんだけど、それっぽい住所や名前をよろしく入力して欲しい、みたいな時。あるいは、DBの初期化時に、多いに力を発揮しそうな感触。

テストコードとして利用する場合、値が毎回ランダムになって使い所がイマイチわかりにくい気もちょっとするが、それで詰まるときってそんなに無いのでは…。値が被って~~~うあああ!!!!みたいなことはあると思う。

話はそれるが最近見かけた property-based testing という方法にはめちゃんこ合っていると思う。

Property-Based Testing for Godly Tests

そもそも PHPUnit なら Data Provider の仕組みがあるし、あるいは CakePHP なら Fixture が、 Laravel には Factory の仕組みがそれぞれにある(きっと他のフレームワークにもあるんじゃないかな、調べてない。。)ので、そういうこともやりやすいと思う。
ちなみに Laravel には faker が既に含まれていて、 Factory の仕組みを使うとガンガンにテストデータを作ってくれる。スゴイ。

Database Testing - Laravel - The PHP Framework For Web Artisans

ORMとの連携はできるとはいえ、既に書いたとおり、各フレームワークがそういう仕組みを提供しているので、それとバッティングするなあという気持ちも。その中で使っていく、みたいなイメージなのかな。。ちょっとわからないや。。

CakePHP3 で論理削除をする

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

CakePHP3 ではデフォルトでは論理削除の機能は実装されていない。調べると1番上にいい感じに利用できるプラグインがあるので、これを利用するとよい。

PGBI/cakephp3-soft-delete

とりあえず PGBI/cakephp3-soft-delete を使ってみる

composer でパッケージを追加して。

$ composer require pgbi/cakephp3-soft-delete "~1.0"

プラグインの読み込みを追加して。

// config/bootstrap.php
Plugin::load('SoftDelete');

テーブルで SoftDeleteTrait を読み込んで。

// src/Model/Table/UserTable.php

...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;
    ...

該当のテーブルに deleted というカラムを追加して。

// config/Migrations/20171114120000_CreateUsers.php
...

$table->addColumn('deleted', 'datetime', [
    'default' => null,
    'null' => true,
]);

...

Enjoy!

// コンソール機能を使って動作確認をしてみる
//==============================
$ bin/cake console


// Table の読み込み
//==============================
>>> use Cake\ORM\TableRegistry;
>>> $Users = TableRegistry::get('Users');
=> App\Model\Table\UsersTable {#214
     +"registryAlias": "Users",
     +"table": "users",
     +"alias": "Users",
     +"entityClass": "App\Model\Entity\User",
     +"associations": [],
     +"behaviors": [
       "Timestamp",
     ],
     +"defaultConnection": "default",
     +"connectionName": "default",
   }


// Entity の作成
//==============================
>>> $entity = $Users->newEntity();
=> App\Model\Entity\User {#216
     +"[new]": true,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データの保存
//==============================
>>> $Users->save($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#218
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データ保存の確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#295
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#286
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#285
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// SoftDeleteTrait を読み込むと delete の挙動が SoftDelete に変わる
//==============================
>>> $Users->delete($entity);
=> true


// SoftDelete されたデータが検索できないことを確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'


// 'withDeleted' をつけることによって SoftDelete されたデータを検索できる
//==============================
>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> App\Model\Entity\User {#521
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#512
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#511
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": Cake\I18n\FrozenTime {#522
       +"time": "2017-11-14T12:42:48+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore によって SoftDelete されたデータを元に戻せる
//==============================
>>> $Users->restore($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#229
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore の結果について確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#548
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#539
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#538
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// hardDelete をすることによって、復元できない削除をする
//==============================
>>> $Users->hardDelete($entity);
=> true


// hardDelete の結果について確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'

>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> null

かゆいところに手を伸ばす

デフォルトでは deleted というカラムだが、テーブルにプロパティを追加することで違うカラム名にもできる。

// UserTable.php
...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;

    protected $softDeleteField = 'kokoga_deleted_no_field_desu';

    ..

とはいえ CakePHP3 におけるデフォルトのタイムスタンプのフィールドは created と modified という名前になっているので、そこは deleted という名前で合わせにいくのがわかりやすさも高くて良いと思う。

Google Docs のスプレッドシートで変更したよ!の通知を送りたい

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

管理シート、複数の人が編集して担当者設定していく〜みたいな使い方をしたいんだけど!!!って言われたので…。

そもそもとして、スプレッドシートには変更したよ!の通知メールを送ることが出来る。

しかしこの機能はドバーっと送るためのもので、例えばこう、担当者が誰になって、このセルが入力されたときに、誰と誰に送る、といった複雑な処理が必要な場合には使えない。そういったときに Google App Script の出番だ。

Google App Script

Apps Script | Google Developers

GAS は端的に言えば現代におけるサーバレス、 AWS Lamda のようなもので、プログラムを Google ドライブ上に置いて、さまざまなトリガーで起動出来る。cron のように定期的に実行したり、 Google Docs と連携して操作が行われたときに実行したり、はたまた Web サイトとして公開することだって出来る。

今回はその中でも、スプレッドシート内のセル値が変更されたとき、に実行する。

というのが無料で出来るんだからグーグルってばすごい。

スプレッドシート x GAS

手順としてはこう。スプレッドシートからスクリプトエディタを開くだけ。

今回やりたい、変更されたとき、という設定はこのように行う。

あとはドキュメントを見ながら格闘して「アクティブなセル(=変更されたセル)がこの A 列だったら」とかそういうコードを書けば勝ち。

Class SpreadsheetApp | Apps Script | Google Developers

GAS から通知を送る

メールなら MailApp というクラスが用意されていて、それを呼ぶだけでメール送信ができる。あるいは Slack などの Webhook な振る舞いを実行するなら UrlFetchApp というクラスを使う。どちらにしても用意されているので簡単だ。

Class MailApp | Apps Script | Google Developers

Class UrlFetchApp | Apps Script | Google Developers

とりあえず、いずれかのセルが変更されたときにメールを送るよ!についてのソースとしてはこんな感じ。

function myFunction() {
  // スプレッドシートを取得
  var activeSheet = SpreadsheetApp.getActiveSheet();
  
  // セル範囲を取得
  var activeRange = activeSheet.getActiveRange();
  
  // メール送信
  var text = '';
  text += activeSheet.getName() + ' の ' + activeRange.getA1Notation() + ' が 変更されました\n';
  text += '新しい値: ' + activeRange.getValue();
  MailApp.sendEmail('....@.....', 'メール通知のテスト', text)  
}

これで設定したセルが変更されたとき、設定された方法で通知を送ることができる。 GAS 便利ちゃん。

便利ちゃんなのだが、メールを送るにあたって注意点がある。自分の持っている、認証されているメールアドレスからのみで送信できる、という話で、適当なメールアドレスからは送れないよというもの。通知としてのメール送信なら Slack 等のチャットの API を実行したら良いんじゃないかなあ。

1 2 3 4 5 6 7 8 9 10 11 12 13