PHPでiOSのレシートを検証したい(超簡易メモ版)

前回はAndroidのレシート検証について超簡易的なメモを残したので、その流れでiOSについても超簡易的なメモを残します。 ohshige.hatenablog.com

参考:
レシート検証プログラミングガイド

iOSの場合、公式の特別なライブラリは無いようなので、PHPからそのままAPIを叩くことにします。

そのためにguzzleを使います。

composer require guzzlehttp/guzzle

開発環境もしくは審査中の場合にはsandbox環境用のURL、本番環境の場合にはproduction環境用のURLが用意されているので、それを使います。

環境 URL
sandbox https://sandbox.itunes.apple.com/verifyReceipt
production https://buy.itunes.apple.com/verifyReceipt
// 環境に応じたURL
$verify_url = 'https://buy.itunes.apple.com/verifyReceipt';

$client = new GuzzleHttp\Client();
$response = $client->request('POST', $verify_url, [
    'json' => [
        'receipt-data' => [base64でエンコードされたレシートデータ],
        'password' => [アプリケーションの共有シークレット],
    ],
]);

if ($response->getStatusCode() !== 200) {
    // 何かしらの通信エラー
    return;
}

$body = json_decode($response->getBody()->getContents(), true);

// sandbox用のレシートがproductionに送信された場合の処理
if ($body['status'] === 21007) {
    // 必要ならsandbox用URLへ再送するなどの処理
}

// レシートが有効でなかった場合の処理
if ($body['status'] !== 0) {
    // 必要なら何かしらの処理
}

// バンドルIDが異なる場合の処理
if ($body['receipt']['bundle_id'] !== [バンドルID]) {
    // 必要なら何かしらの処理
}

// レシート検証に成功した場合の処理

PHPでGoogle Play Developer APIを使ってAndroidのレシートを検証したい(超簡易メモ版)

GoogleAPIを操作するためのPHP向けライブラリとしてgoogle-api-php-clientがありますが、Androidのレシート検証に際して使い方がイマイチわからなかったので、メモとして残します。 github.com

前提として、Google Play Consoleを使って、OAuthトークンやリフレッシュトークンなどをすでに取得しているものとします。
今回はOAuth認証の場合のライブラリ操作だけを残します。

まずは、ライブラリをインストールします。

composer require google/apiclient

アクセストークンの発行はライブラリが良しなにやってくれるので、以下のようにパブリッシャーを用意します。

$client = new \Google_Client();
$client->setAuthConfig([認証用JSONを設置したパス]);
$client->setRedirectUri('http://localhost');
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->addScope(\Google_Service_AndroidPublisher::ANDROIDPUBLISHER);
$client->refreshToken([リフレッシュトークン]);
 
$publisher = new \Google_Service_AndroidPublisher($client);

あとは、定期購読と都度課金のどちらの課金なのかに応じて呼び分けるだけです。

// 定期購読の場合
$result = $publisher->purchases_subscriptions->get([packageName], [productId], [purchaseToken]);
// 都度課金の場合
$result = $publisher->purchases_products->get([packageName], [productId], [purchaseToken]);

【完成版】Googleカレンダーの条件に合う予定が終了したらメンション付きで概要を通知したい(IFTTT + GAS + Slack)

はじめに

知人から以下のような相談を受けました。

Googleカレンダーの決まった予定が終わったタイミングで、その予定の詳細を予定参加者にSlackで通知したいが、簡単にできる方法を知らないか?

IFTTTとSlackの連携や簡単なGASなら知識はあるようでしたが、既存サービスの連携だけでは簡単には実現できなさそうでした。
そこで、ブログのネタを探してもいたので、自分で作ってみることにしました。

前回の続きのようなものです。 ohshige.hatenablog.com

仕様と方針

微妙に改変していますが、具体的な仕様は以下の通りです。

Googleカレンダーの詳細(「説明を追加」の欄)に「要確認」が含まれている予定が終了したタイミングで、リマインダーのために、その予定のゲストへのメンション付きで詳細文全体をslackへ投稿する。

難しいことはしたくないので、予定終了タイミングの検知はIFTTT、Slack通知のための諸々の調整はGASでまとめて行うことにしました。

IFTTTには、「Googleカレンダーの予定のうち特定のキーワードを含むような予定が終了するタイミング」というトリガーがあるので、これを使います。「Google Calendar」→「New event from search ends」です。
ただ、これには問題があり、受け取れる情報が「タイトル」「詳細」「場所」「開始日時」「終了日時」「イベントURL」だけで、メンションを付けるために必要なゲストの情報を取ることができません。
なので、GAS側でGoogleカレンダーと連携してゲスト情報を取得する必要があります。
それさえできれば、最終的には文章を組み立てて、SlackへAPI経由で投稿するだけです。

メンション無しでSlackへ投稿

まずはメンション無しでSlackへ投稿する部分を作り、その後でメンションも含めて投稿できるように修正することにします。

IFTTT

まず、IFTTTの「New Applet」からAppletを作ります。

「if this」は「Google Calendar」を選び、トリガーは「New event from search ends」にします。
適切な自分のGoogleカレンダーを選択し、キーワードとして「要確認」を設定します。

「then that」は「Webhooks」を選び、トリガーは(1つしか無いものの)「Make a web request」にします。
そして、各項目を以下のように設定します。
タイトルと詳細はBodyとして&連結でエスケープして送信します。

項目 内容
URL GASの公開URL
Method POST
Content Type application/x-www-form-url
Body <<<{{Title}}>>>&<<<{Description}>>>

GAS

以下のように書いてSlackへ通知します。

function doPost(e) {
  var params = e.postData.contents.split('&');
  var title = decodeURIComponent(params[0].replace(/\+/g, ' '));
  var description = decodeURIComponent(params[1].replace(/\+/g, ' '));
  notifyToSlack(title, description);
}

function notifyToSlack(title, description) {
  var message = '「' + title + '」の予定が終わりました。\n以下をご確認下さい。\n---\n' + description;
  postSlack(message);
}

function postSlack(message) {
  var data = {
    'text': message
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : JSON.stringify(data)
  };
  UrlFetchApp.fetch('https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', options);
}

試してみる

「要確認」というキーワードを含めた上で適当に予定を作り、その予定が終わるのを待つと、Slackへ通知が来ます。

メンション付きでSlackへ投稿

メンション無しでの投稿はできたので、続いてメンション付きでやってみます。

メンションを付けるためにはその予定のゲスト情報を取得する必要があります。
IFTTTからはゲスト情報を送信することができないので、GASでなんとかしてゲスト情報を取得する必要があります。
それさえできれば、その情報を使ってSlackに送信するだけです。

GASでGoogleカレンダーの情報を取得

リファレンスによるとGoogleカレンダーの1つ1つの予定はCalendarEventというクラスに対応しているようです。 developers.google.com そして、そのメソッドとしてgetGuestList()というものがあり、これを使えばゲスト情報を取得できます。

というわけで、IFTTTから送れる情報とGASの何らかのAPIを使ってCalenderEventを定める必要があります。
developers.google.com

GASで使えるAPIとして、Calendar.getEventById(iCalId)というものがあります。
iCalIDなる識別子を渡すことができれば良さそうですが、IFTTTから送ることのできる情報にはないので無理そうです。
IFTTTからはイベントURLを送ることはできますが、このURLに含まれる識別子とは別のものなのでパースして云々という方法ではできません。

というわけで、Calendar.getEvents(startTime, endTime, options)を使います。
これは、開始日時と終了日時から予定を検索するものです。
さらに、オプションとしてキーワード検索も含めることができます。

以下のようにして、カレンダーの予定を探し出します。

function findCalendarEvent(startTime, endTime, keyword) {
  var options = {
    'max': 1,
    'search': keyword
  }
  var calEventList = CalendarApp.getDefaultCalendar().getEvents(startTime, endTime, options);

  if (calEventList.length === 0) {
    return null;
  }

  return calEventList[0];
}

optionsに含まれるmaxは最大1件のみを返すという意味です。
同じ時間帯に「要確認」の複数の予定が入っていた場合は困りますが、もともとの仕様を考慮すると基本的に起こりえないこととして無視します。

これでカレンダーの予定が特定できたのであとはゲストを取得するだけで済みそうですが、そうでもありません。

上記のfindCalendarEvent(startTime, endTime, keyword)startTimeendTimeはDateオブジェクトを想定しています。
IFTTTから送られてくる日時のフォーマットはJune 24, 2019 at 07:00PMのようになっていて、このままだとDateオブジェクトに変換するときにうまくパースしてくれません。
そこで、この日時の文字列をDateオブジェクトが認識できる形に変換する必要があります。
Dateオブジェクトが認識してくれつつ、可能な限り簡潔に変換するために、以下のような関数を用意します。

function convertDate(dateString) {
  var meridiem = dateString.slice(-2);
  dateString = dateString.replace('at ', '').slice(0, -2);
  var date = new Date(dateString);
  if (meridiem === 'PM') {
    date.setHours(date.getHours() + 12);
  }
  return date;
}

June 24, 2019 at 07:00PMといった文字列がJune 24, 2019 07:00というDateオブジェクトのパーサーに理解可能な形に変換され、最後にPMの場合は12時間分加算されるという流れです。

以上から、IFTTTから送られるタイトルがtitleで開始日時がstartで終了日時がendだとすると、以下のように実行すれば、その予定を特定できます。

findCalendarEvent(convertDate(start), convertDate(end), title)

ゲスト情報の取得とメンション

カレンダーの予定を取得することはできたので、続いて、そこからゲスト情報を取得した上で、最終的に投稿する際に必要なメンションへの変換を行います。
方針としては、getGuestList()でゲスト情報を取得しつつ、これをループで回して、メールアドレスとメンションの情報を連想配列で保持しておき、そこから取得するという流れにします。

以下のように、notifyToSlack(title, description)の修正とslackMentionIdsの追加を行います。

slackMentionIds = {
  'ohshige@sample.com': 'UAAAAAAAA',
  'tanaka@sample.com': 'UBBBBBBBB',
  'suzuki@sample.com': 'UCCCCCCCC',
  'takahashi@samole.com': 'UDDDDDDDD',
}

function notifyToSlack(title, description, start, end) {
  var calendarEvent = findCalendarEvent(convertDate(start), convertDate(end), title);
  if (calendarEvent === null) {
    return;
  }

  var mentions = [];
  calendarEvent.getGuestList().forEach(function(guest) {
    var email = guest.getEmail();
    if (slackMentionIds[email]) {
      mentions.push('<@' + slackMentionIds[email] + '>');
    }
  }, mentions);

  var message = mentions.join(' ') + '\n「' + title + '」の予定が終わりました。\n以下をご確認下さい。\n---\n' + description;

  postSlack(message);
}

メールアドレスとSlackでのメンバーIDの対応をslackMentionIdsに設定しておくことで、メンションが必要なゲストを特定しています。
これは連想配列ではなくスプレッドシートで管理することもできて、その方が運用は楽な気がしますが、今回はそこまでやりません。

注意点として、getGuestList()では予定作成者は返却されないので結果的にメンションに予定作成者が含まれることはありません。
getGuestList(includeOwner)というメソッドもあるので、予定作成者が必要ならそちらを使うと良いと思います。

IFTTTの修正とGASの最終版

IFTTTから送るべき情報が増えたので、以下のようにIFTTTの設定を修正します。

項目 内容
URL GASの公開URL
Method POST
Content Type application/x-www-form-url
Body <<<{{Title}}>>>&<<<{{Description}}>>>&<<<{{StartTime}}>>>&<<<{{EndTime}}>>>

そして、IFTTTからのPOSTを受け取れるように修正した上でのGASの最終版が以下です。

slackMentionIds = {
  'ohshige@sample.com': 'UAAAAAAAA',
  'tanaka@sample.com': 'UBBBBBBBB',
  'suzuki@sample.com': 'UCCCCCCCC',
  'takahashi@samole.com': 'UDDDDDDDD',
}

function doPost(e) {
  var params = e.postData.contents.split('&');
  var title = decodeURIComponent(params[0].replace(/\+/g, ' '));
  var description = decodeURIComponent(params[1].replace(/\+/g, ' '));
  var start = decodeURIComponent(params[2].replace(/\+/g, ' '));
  var end = decodeURIComponent(params[3].replace(/\+/g, ' '));
  notifyToSlack(title, description, start, end);
}

function notifyToSlack(title, description, start, end) {
  var calendarEvent = findCalendarEvent(convertDate(start), convertDate(end), title);
  if (calendarEvent === null) {
    return;
  }

  var mentions = [];
  calendarEvent.getGuestList().forEach(function(guest) {
    var email = guest.getEmail();
    if (slackMentionIds[email]) {
      mentions.push('<@' + slackMentionIds[email] + '>');
    }
  }, mentions);

  var message = mentions.join(' ') + '\n「' + title + '」の予定が終わりました。\n以下をご確認下さい。\n---\n' + description;

  postSlack(message);
}

function findCalendarEvent(startTime, endTime, keyword) {
  var options = {
    'max': 1,
    'search': keyword
  }
  var calEventList = CalendarApp.getDefaultCalendar().getEvents(startTime, endTime, options);

  if (calEventList.length === 0) {
    return null;
  }

  return calEventList[0];
}

function convertDate(dateString) {
  var meridiem = dateString.slice(-2);
  dateString = dateString.replace('at ', '').slice(0, -2);
  var date = new Date(dateString);
  if (meridiem === 'PM') {
    date.setHours(date.getHours() + 12);
  }
  return date;
}

function postSlack(message) {
  var data = {
    'text': message
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : JSON.stringify(data)
  };
  UrlFetchApp.fetch('https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', options);
}

試してみる

「要確認」というキーワードを含めた上で適当に予定を作り、その予定が終わるのを待つと、Slackへ通知が来ます。
予定作成者が自分でゲストが誰もいないとメンションは含まれないので、適当にゲストを追加した上でそのメールアドレスとSlackでのメンバーIDの対応をslackMentionIdsに追加する必要があります。
これで完成です!

おわりに

ゲスト情報を取るためにカレンダーの予定を1つに特定する方法が意外と面倒で大変でした。
もしかしたら、他に簡単に取得できる方法があるかもしれませんが、あればどなたか教えて下さい。

とりあえず、知人からの依頼は達成でき、ちゃんと稼働しているようです。
お礼になにか奢ってもらいます!

IFTTTとGASとSlackを連携してGoogleカレンダーの予定が終わったら通知したい

はじめに

IFTTTとGoogle Apps Scriptを使って、Googleカレンダーの予定が終了したタイミングでSlackに通知してみます。

Googleカレンダーに登録されている予定が終了したタイミングで、Slackに

@ohshige
「〜〜〜」の予定が終わりました。

と通知してみます。

IFTTT + GAS + Slack

前提

この続きみたいなものです。 ohshige.hatenablog.com

GASのプロジェクトとSlack APIを準備しておきます。
また、IFTTTのアカウントも用意しておきます。

IFTTT

まず、IFTTTの「New Applet」からAppletを作ります。

「if this」は「Google Calendar」を選び、トリガーは「Any event ends」にします。

「then that」は「Webhooks」を選び、トリガーは(1つしか無いものの)「Make a web request」にします。
そして、各項目を以下のように設定します。
Bodyでは<<<{{Title}}>>>とすることによって、Googleカレンダーのタイトルをエスケープした状態で送信できます。

項目 内容
URL GASの公開URL
Method POST
Content Type application/x-www-form-url
Body <<<{{Title}}>>>

f:id:ohshige:20190616233419p:plain:w400

GAS

IFTTTからはPOSTでリクエストしているので、GASではdoPostファンクションを使います。

リクエストボディはe.postData.contentsで受け取れますが、エスケープされているので、+をスペースに変換した上で、decodeURIComponentにかけます。

後は、そのまま、リクエストボディから得たタイトルをSlackに投稿するだけです。

function doPost(e) {
  var title = decodeURIComponent(e.postData.contents.replace(/\+/g, ' '));
  notifyToSlack(title);
}

function notifyToSlack(title) {
  var data = {
    'text': '<@ABCXXXXXX>\n「' + title + '」の予定が終わりました。\n'
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : JSON.stringify(data)
  };
  UrlFetchApp.fetch('https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', options);
}

これで、IFTTTの設定とGASが準備できたので、Googleカレンダーである予定が終了したら(もしくは終了時間近くで)IFTTTが発火してSlackに通知が来ます。

おわりに

このままだと、全ての予定について終了したタイミングで通知が来てしまいます。
それで問題ない場合は良いですが、例えば「予定の詳細に特定の文字が含まれる場合だけ」といった条件が付く場合はこのままだと都合が悪いので改修が必要になります。
それは次回書く予定です。

そもそも、全ての予定について終了したタイミングでSlackに通知するなら、GASなんて使わずにIFTTTだけで事足りますが、次回への布石です。

GASを使ってSlackにメンション付きで投稿したい

はじめに

Google Apps Script から Slack API を呼んでメンション付きで特定のチャンネルに投稿してみたのでまとめます。

Slack API

Incoming Webhooks の URL を取得

まずは、 Incoming Webhooks の設定をする必要があります。

このページに従って進めれば簡単につくれます。
api.slack.com

具体的には、まず、まだ持っていないならここから Slack App を作ります。
続いて、 Incoming Webhooks を Activate します。offからonにするだけ。
最後に、Add New Webhook to Workspace を押して、投稿したいチャンネルを選んだら、Webhook用のURLが用意されます。

メンション付きで投稿してみる

丁寧にサンプルも用意されているので試しにSlackに投稿してみます。
成功すれば、curlの返却としては ok が返ってきて、確かにslackに投稿されているのが確認できます。

curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

続いて、 @ohshige というユーザにメンション付きで送ってみます。
確かに投稿はできていますが、メンションとしては有効になっていないことがわかります。

curl -X POST -H 'Content-type: application/json' --data '{"text":"@ohshige\nHello, World!"}' https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

メンション付きで投稿するためには、まずそのユーザのメンバーIDというものが必要になります。
そのユーザのプロフィールの「...」を選択すると「メンバーIDをコピー」という箇所があるので、そこからコピーすると9文字程度の文字列が取得できます。

f:id:ohshige:20190609182345p:plain:w400

メンバーIDが「ABCXXXXXX」だったとすると、メンションするためには <@ABCXXXXXX> と記述する必要があります。
これで、メンション付きで投稿できます。

curl -X POST -H 'Content-type: application/json' --data '{"text":"<@ABCXXXXXX>\nHello, World!"}' https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

Google Apps Script から Slack に投稿する

Google Apps Script の詳しい使い方は省きますが、関数内で UrlFetchApp.fetch() を使うだけです。

以下のようにコードを記述します。

function doGet(e) {
    var data = {
    'text': '<@ABCXXXXXX>\nHello, World!'
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : JSON.stringify(data)
  };
  UrlFetchApp.fetch('https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', options);
}

あとは、公開して、払い出されたURLにGETでリクエストすればSlackにメンション付きで投稿されます。

おわりに

今はもうエンジニアでなくてもGASやSlack Botなどを使って簡単に効率化できるようになっていて、良い時代だと思います。
使えるものはどんどん使って便利な日々にしていきたいです。

言語処理100本ノック 第4章: 形態素解析 (後編)

はじめに

今回は 言語処理100本ノック 第4章: 形態素解析 の後編です。

これまでの

ohshige.hatenablog.com ohshige.hatenablog.com

第4章: 形態素解析

Python 3.7.0 でやっていきます。 問題の解釈違い、間違い等ありましたら、教えていただけると幸いです。

github.com

前回の続きです。

35. 名詞の連接

問題

名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.

解答&出力

from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

result = []

for morphemes in morphemes_list:
    noun_list = []
    for morpheme in morphemes:
        if morpheme["pos"] == "名詞":
            noun_list.append(morpheme["surface"])
        else:
            if len(noun_list) > 1:
                result.append("".join(noun_list))
            noun_list = []
    else:
        if len(noun_list) > 1:
            result.append("".join(noun_list))

print(result[:10])
['人間中', '一番獰悪', '時妙', '一毛', 'その後猫', '一度', 'ぷうぷうと煙', '邸内', '三毛', '書生以外']

ひとこと

順番に見ていって、名詞が連続している限り配列に格納して、名詞でないときにその配列を連結させるようにしています。
結果を全て出力すると大変なので、最初の10件だけを出力しています。(以降、同様)

36. 単語の出現頻度

問題

文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.

解答&出力

from collections import Counter
from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

words = Counter([morpheme["base"] for morphemes in morphemes_list for morpheme in morphemes]).most_common()
print(words[:10])
[('の', 9194), ('。', 7486), ('て', 6848), ('、', 6772), ('は', 6420), ('に', 6243), ('を', 6071), ('だ', 5975), ('と', 5508), ('が', 5337)]

ひとこと

collections.Counterを使えば簡単に出現頻度を集計してくれるので楽です。
さらに、most_common()メソッドを使えば、出現頻度順に並べた上で特定の頻度以上のペア(出現対象と頻度)を抽出できるので大変便利です。

37. 頻度上位10語

問題

出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.

解答&出力

from collections import Counter
import matplotlib.pyplot as plt
from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

words = Counter([morpheme["base"] for morphemes in morphemes_list for morpheme in morphemes]).most_common()
word_name, word_count = list(zip(*words[:10]))

plt.rcParams["font.family"] = "IPAexGothic"
plt.bar(range(10), word_count, tick_label=word_name)
plt.savefig("fig37.png")

f:id:ohshige:20190529213330p:plain:w500

ひとこと

matplotlibの使い方で、詰まった部分がいくつかあったので他のブログ様を参考にしました。

from matplotlib.backends import _macosx

が出たときの対処はこちらを参考にしました。
Pythonでmatplotlibをimportするとエラーが出る場合の対処策(Mac) - Qiita

文字化けしてしまったときの対処はこちらを参考にしました。
matplotlibの文字化け問題でハマった時のメモ - Qiita

基本的には問題36を流用して、結果を棒グラフとして出力するようにしただけです。

38. ヒストグラム

問題

単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.

解答&出力

from collections import Counter
import matplotlib.pyplot as plt
from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

words = Counter([morpheme["base"] for morphemes in morphemes_list for morpheme in morphemes]).most_common()
_, word_count = list(zip(*words))

plt.rcParams["font.family"] = "IPAexGothic"
plt.hist(word_count, bins=50, range=(1, 50))
plt.savefig("fig38.png")

f:id:ohshige:20190529213835p:plain:w500

ひとこと

これも基本的には問題36を流用して、結果をヒストグラムとして出力するようにしただけです。
ですが、そのままやってしまうと出現頻度が偏りすぎていて情報量がほぼない結果になってしまうので、ビンの数を50に増やして(デフォルトだと10)、範囲を1〜50に狭めました。
対数をとっても良かったかもしれないです。

39. Zipfの法則

問題

単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.

解答&出力

from collections import Counter
import matplotlib.pyplot as plt
from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

words = Counter([morpheme["base"] for morphemes in morphemes_list for morpheme in morphemes]).most_common()
_, word_count = list(zip(*words))

plt.rcParams["font.family"] = "IPAexGothic"
plt.plot(list(range(1, len(word_count) + 1)), word_count)
plt.xscale("log")
plt.yscale("log")
plt.savefig("fig39.png")

f:id:ohshige:20190529214341p:plain:w500

ひとこと

これも基本的には問題36を流用して、出現頻度順位と出現頻度を対数軸にプロットしただけです。
うまくZipfの法則が現れていると思います。
ジップの法則 - Wikipedia

おわりに

4章の後半はmatplotlibの使い方が慣れておらず難しいだけで、実装自体は特に悩むこともなくさくさくできました。

最近、100本ノックのもくもく会に参加できておらず、全然進んでいないのですが、引き続き頑張っていきたいです。

続き

まだ

言語処理100本ノック 第4章: 形態素解析 (前編)

はじめに

今回は 言語処理100本ノック 第4章: 形態素解析 の前編です。

これまでの

ohshige.hatenablog.com ohshige.hatenablog.com

第4章: 形態素解析

Python 3.7.0 でやっていきます。 問題の解釈違い、間違い等ありましたら、教えていただけると幸いです。

github.com

指定の「吾輩は猫である」のテキストをダウンロードしてneko.txtとして保存し、mecabをインストールした状態で、

cat neko.txt | mecab > neko.txt.mecab

として、neko.txt.mecabを準備した状態です。

30. 形態素解析結果の読み込み

問題

形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.
ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素マッピング型)のリストとして表現せよ.
第4章の残りの問題では,ここで作ったプログラムを活用せよ.

解答&出力

def get_neko_morphemes():
    morphemes_list = []
    with open("neko.txt.mecab") as f:
        morphemes = []
        for i, line in enumerate(f):
            split_line = line.rstrip("\r\n").split("\t")
            if len(split_line) == 1:
                if len(morphemes) != 0:
                    morphemes_list.append(morphemes)
                    morphemes = []
                continue
            morpheme_map = split_line[1].split(",")
            morpheme = {
                "surface": split_line[0],
                "base": morpheme_map[6],
                "pos": morpheme_map[0],
                "pos1": morpheme_map[1],
            }
            morphemes.append(morpheme)
        else:
            if len(morphemes) != 0:
                morphemes_list.append(morphemes)

    return morphemes_list


if __name__ == "__main__":
    from pprint import pprint
    result = get_neko_morphemes()
    pprint(result[:10])
[[{'base': '一', 'pos': '名詞', 'pos1': '数', 'surface': '一'}],
 [{'base': '\u3000', 'pos': '記号', 'pos1': '空白', 'surface': '\u3000'},
  {'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞', 'surface': '吾輩'},
  {'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
  {'base': '猫', 'pos': '名詞', 'pos1': '一般', 'surface': '猫'},
  {'base': 'だ', 'pos': '助動詞', 'pos1': '*', 'surface': 'で'},
  {'base': 'ある', 'pos': '助動詞', 'pos1': '*', 'surface': 'ある'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}],

〜

  {'base': '感じ', 'pos': '名詞', 'pos1': '一般', 'surface': '感じ'},
  {'base': 'が', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'が'},
  {'base': 'ある', 'pos': '動詞', 'pos1': '自立', 'surface': 'あっ'},
  {'base': 'た', 'pos': '助動詞', 'pos1': '*', 'surface': 'た'},
  {'base': 'ばかり', 'pos': '助詞', 'pos1': '副助詞', 'surface': 'ばかり'},
  {'base': 'だ', 'pos': '助動詞', 'pos1': '*', 'surface': 'で'},
  {'base': 'ある', 'pos': '助動詞', 'pos1': '*', 'surface': 'ある'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}]]

ひとこと

1行ずつ読み込み、タブとカンマ(,)で区切って、表層形はタブ区切りの左、品詞はタブ区切りの右のうちのカンマ区切りの1つ目など、mapに入れていくだけです。
結果を全て出力すると大変なので、最初の10件だけを出力しています。(以降、同様)

31. 動詞

問題

動詞の表層形をすべて抽出せよ.

解答&出力

from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

result = []

for morphemes in morphemes_list:
    for morpheme in morphemes:
        if morpheme["pos"] == "動詞":
            result.append(morpheme["surface"])

print(result[:10])
['生れ', 'つか', 'し', '泣い', 'し', 'いる', '始め', '見', '聞く', '捕え']

ひとこと

問題30を利用して、ループで回して品詞(pos)が「動詞」であるものの表層系(surface)を結果に格納しているだけです。

32. 動詞の原形

問題

動詞の原形をすべて抽出せよ.

解答&出力

from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

result = []

for morphemes in morphemes_list:
    for morpheme in morphemes:
        if morpheme["pos"] == "動詞":
            result.append(morpheme["base"])

print(result[:10])
['生れる', 'つく', 'する', '泣く', 'する', 'いる', '始める', '見る', '聞く', '捕える']

ひとこと

31と同じように、ループで回して品詞(pos)が「動詞」であるものの原形(base)を結果に格納しているだけです。

33. サ変名詞

問題

サ変接続の名詞をすべて抽出せよ.

解答&出力

from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

result = []

for morphemes in morphemes_list:
    for morpheme in morphemes:
        if morpheme["pos"] == "名詞" and morpheme["pos1"] == "サ変接続":
            result.append(morpheme["surface"])

print(result[:10])
['見当', '記憶', '話', '装飾', '突起', '運転', '記憶', '分別', '決心', '我慢']

ひとこと

ループで回して、品詞(pos)が「名詞」であり且つ品詞細分類1(pos1)が「サ変接続」であるものを結果に格納しています。
問題に指定はありませんが、表層系を格納しています。

34. 「AのB」

問題

2つの名詞が「の」で連結されている名詞句を抽出せよ.

解答&出力

from div04.sec30 import get_neko_morphemes

morphemes_list = get_neko_morphemes()

result = []

for morphemes in morphemes_list:
    for i in range(1, len(morphemes) - 1):
        if morphemes[i]["surface"] != "の":
            continue
        before = morphemes[i - 1]
        after = morphemes[i + 1]
        if before["pos"] != "名詞" or after["pos"] != "名詞":
            continue
        result.append(before["surface"] + "の" + after["surface"])

print(result[:10])
['彼の掌', '掌の上', '書生の顔', 'はずの顔', '顔の真中', '穴の中', '書生の掌', '掌の裏', '何の事', '肝心の母親']

ひとこと

「の」を探した上で、その前後が「名詞」であるものを抽出しています。
連続した3つの形態素を抽出することになるので、「の」探索の範囲(range(1, len(morphemes) - 1))は最初と最後を除いています。

おわりに

3章は難しかったですが、4章の少なくとも前半は割りと簡単でした。
mecabbrewなどで簡単にインストールできるので、形態素解析に入門するのも簡単なご時世です。

続き

まだ