DMM英会話では大半の教材がブラウザで表示できる形になっています。ですが教材ページのhtmlでの装飾が少し見づらいなと感じることがあります。このため、余計な情報を全部カットしてcsv形式に整形し直してみました。
本記事の対象読者
この記事は、下記の方を対象としています。
(cliでのコマンドがわかる方向けの記事なので、万人向けの情報ではないことにご注意ください)
- 情報をブラウザで見るよりcliで確認できる方が便利だと思っている
- macOSを利用している (又はコマンドを見てlinuxコマンド等に読み替えることができる)
- jsonについてある程度知識がある
- curl、jqコマンドが使える環境が用意できている
教材データの構造
作業を始めるあたり、大前提としてDMM英会話の教材のデータ階層を説明します。
まず教材は以下の階層構造になっています。
教材: materials
>コース: course
>レッスン: lesson
さらにそれぞれのレッスンは下記の階層構造になっています。
エクササイズ: exercises
>セクション: sections
あらかじめいくつかの教材ページを試しに見ておくと、この階層構造をイメージしやすいかと思います。
教材一覧画面のURL
教材はたくさんあるので、今回は語彙
の教材だけ整形してみることにします。
下記のページから、句動詞とイディオムのコースが含まれているVocabulary 語彙
教材のページにアクセスできます。
https://eikaiwa.dmm.com/app/materials/vocabulary/9uTJSsYPEeixthfMCuukyg
教材一覧取得のAPI
画面からの表示は前述のURLでしたが、下記のAPIを呼ぶことでコース内のレッスン一覧に関する情報だけが取得できます。
語彙教材には、2つのコースがあるのでURLも2つあります。
コース:句動詞
コース:熟語
教材一覧取得のAPIから、IDとレッスンタイトルのみをcsvで出力
APIの戻り値は膨大でそのままでは少し見づらいです。このためjqコマンドを使って、レッスンのマスタIDと教材名の一覧のみをtsv形式で表示させてみます。
コマンド
COURCE_ID=4d774d54-c611-11e8-9626-a7d3dbc1874c # 句動詞
# COURCE_ID=5bfc0676-c611-11e8-8186-5f78fef0982a # イディオム
# APIのエンドポイント
BASE_URL="https://api.eikaiwa.dmm.com/api"
API_PARAMS="allow_license_partners[]=associatedPress&published_latest=true"
LESSONS_URL="$BASE_URL/courses/$COURCE_ID/lesson_headers?$API_PARAMS"
# APIをコールして必要な情報だけをcsv出力
curl -s $LESSONS_URL |
jq -r '.data[] | [.master_id, .title_text.text] | @tsv' |
cat -n
csv形式にしたい場合は、コマンド中の@tsv
を@csv
にすればよいです。
実行結果
句動詞は40レッスンあるので40行の結果が出力できていれば良いです。
ブラウザでの表示は折りたたみなどがあって少し見づらいですが、これでコマンドラインから一覧が表示され、分かりやすくなりました。
1 a6d6e6aa-503a-11e7-a2d0-bfb1e3c76fdd Wipe Off | Stay Out Of | Clean Up | Take In | Look Up
2 c1959200-50eb-11e7-a926-6f9f84d9e1fc Rip Off |Wear Out |Pull Out | Turn Out | Catch On
3 040ce210-50f5-11e7-a90b-5f7978a6e316 Call Around | Catch Up | Give In | Show Off | Work Out
4 ad2b1b1c-5197-11e7-a914-5fdc793b97f2 Get Along | Back Out | Go For | Find Out | Wear Down
5 fc7ea1ec-51de-11e7-adc6-079ff2b90680 Sort Out | Settle For | Count On | Fall Behind |
...
40 28fb482e-67b5-11e7-8f11-1bffd5f3b817 Hold Back | Draw Up | Step Down | Pass On | Make Up```
ここで表示させたレッスンのマスタID
ですが、これは後で使います。
レッスン
レッスンページのURL
次にレッスンページについて確認します。例えば句動詞のレッスン1の場合だと教材ページは以下のURLでアクセスできます。
URL中にあるlessonsの後の文字列は任意なので、以下の形でもレッスンページへはアクセスできます。
レッスン内容取得のAPI
先程取得したマスタIDをもとに、api/lessons/
から教材内容をjson形式で取得できます。
例えば、コース:ボキャブラリー
のレッスン1
のマスタIDは、'a6d6e6aa-503a-11e7-a2d0-bfb1e3c76fdd'なので、下記のURLで教材内容を取得できます。
- https://api.eikaiwa.dmm.com/api/lessons/a6d6e6aa-503a-11e7-a2d0-bfb1e3c76fdd/current
レッスンのjsonをファイルに出力
レッスンの情報も出力が膨大なので、一時的にファイル出力しておきます。
LESSON_ID='a6d6e6aa-503a-11e7-a2d0-bfb1e3c76fdd'
curl "https://api.eikaiwa.dmm.com/api/lessons/$LESSON_ID/current" > lesson_01.json
jsonが正しく出力できているか、jqコマンドで整形して確認します。
cat lesson_01.json | jq
エクササイズ、セクション
jsonファイルから単語(熟語)の意味についての情報だけを表示
ブラウザからだとボキャブラリーの一覧が少し見づらいので、こちらもレッスン一覧と同様にtsv形式で表示させてみます。
句動詞の英語と日本語訳はjson中の、data.exercises[].sections[].vocab_section_word
あたりにあるので、必要な情報だけをフィルタリングします。
フィルタリング式が少し複雑なので、あえて2つのjqコマンドに分けて記載しています。
セクションは説明文なども含まれていてセクションごとに構造が違います。今回はvocab_section_words
ノードを取得したいのですが、セクションによってはこのノードが存在しない場合があります。
vocab_section_words
が存在しないノードのみをスキップするために、jqのパラメータにselect(. != null)
を追加して該当ノードが存在しない時のフィルタリングを行っています。
cat lesson_01.json |
jq '.data.exercises[].sections[].vocab_section_words | select(. != null)' |
jq -r '.[].local_word | [.word, (.translations[].translations | join("、")) ] | @tsv' |
cat -n
レッスン1のボキャブラリーは5つあるので、全部出力できていることを確認します。
1 wipe off 拭き取る、拭い去る
2 stay out of ~を避ける、~にかかわらない
...
5 look up (言葉などを)調べる
単語の一覧だけが表示され、大変見やすくなりました。
例文だけ取得
例文の方も少し見づらいので、こちらもtsvで必要な情報だけを出力させます。
cat lesson_01.json |
jq '.data.exercises[].sections[].vocab_section_words | select(. != null)' |
jq -r '.[].vocab_section_word_sentences[].local_sentence | [.text, .translations[0].translation] | @tsv'
Use this cloth to wipe off that stain. その染みを拭き取るのに、この布を使いなさい。
Could you please wipe the table off? テーブルを拭いていただけますか。
...
I need to look that word up in a dictionary. 私は辞書でその単語を調べる必要がある。
これで、画面の余計な装飾を除いて必要な情報だけを表示させることができました。
phpのコードに変換
まとめて実行したい時のために、ここまで実行していたコマンド達をphpスクリプトの形に変換しました。
スクリプト内部でshell_exec
関数を利用しているので、流用したい場合はセキュリティに注意してください。
dmm-materials.php (クリックで展開)
<?php
declare(strict_types=1);
namespace App;
use Generator;
class DmmMaterial
{
private const BASE_URL = "https://api.eikaiwa.dmm.com/api";
/**
* レッスンの一覧を取得
*/
public function getLessons(string $courceId): Generator
{
$url = $this->getUrl(
"courses/{$courceId}/lesson_headers",
"allow_license_partners[]=associatedPress&published_latest=true",
);
$command = "curl -s '{$url}' | jq -r '.data[] | [.master_id, .title_text.text] | @tsv'";
return $this->tsvToObj($command, ['lessonId', 'title']);
}
/**
* 単語・熟語の一覧を取得
*/
public function getWords(string $lessonId): Generator
{
$this->log("getWords: ${lessonId}");
$url = $this->getUrl("lessons/${lessonId}/current");
$command = "curl -s '{$url}' |";
$command .= "jq '.data.exercises[].sections[].vocab_section_words | select(. != null)' |";
$command .= "jq -r '.[].local_word | select(. != null) | [.word, .definition, (.translations[].translations | join(\"、\")) ] | @tsv'";
return $this->tsvToObj($command, ['ja', 'definition', 'en']);
}
/**
* 単語・熟語の例文一覧を取得
*/
public function getWordSentences(string $lessonId): Generator
{
$this->log("getWordSentences: ${lessonId}");
$url = $this->getUrl("lessons/${lessonId}/current");
$command = "curl -s '{$url}' |";
$command .= "jq '.data.exercises[].sections[].vocab_section_words | select(. != null)' |";
$command .= "jq -r '.[].vocab_section_word_sentences[].local_sentence | select(. != null) | [.text, .translations[0].translation] | @tsv'";
return $this->tsvToObj($command, ['ja', 'en']);
}
/**
* URLを取得
*/
private function getUrl(string $api, string $param = null): string
{
$url = sprintf('%s/%s', self::BASE_URL, $api);
if (!is_null($param)) {
$url .= '?' . $param;
}
return $url;
}
/**
* tsvテキストをオブジェクトの一覧に変換
*/
private function tsvToObj(string $command, array $columnNames): Generator
{
$this->log(' cmd = ' . $command);
$lines = shell_exec($command);
if (is_null($lines)) {
return;
}
foreach (explode(PHP_EOL, $lines) as $line) {
$colmuns = explode("\t", $line);
if (count($colmuns) < count($columnNames)) {
continue;
}
$row = new \stdClass();
$offset = 0;
foreach ($columnNames as $name) {
$row->$name = $colmuns[$offset++];
}
$this->log(" " . json_encode($row, JSON_UNESCAPED_UNICODE));
yield $row;
}
}
private function log(string $message): void
{
// fputs(STDERR, $message . PHP_EOL);
}
}
function main(string $courceId): void
{
$material = new DmmMaterial();
$lessonNo = 0;
foreach ($material->getLessons($courceId) as $lesson) {
$lessonNo++;
foreach ($material->getWords($lesson->lessonId) as $row) {
printf("%s\t%d\t%s\t%s\t%s\n", 'W', $lessonNo, $row->ja, $row->en, $row->definition);
}
usleep(1000 * 1000);
}
$lessonNo = 0;
foreach ($material->getLessons($courceId) as $lesson) {
$lessonNo++;
foreach ($material->getWordSentences($lesson->lessonId) as $row) {
printf("%s\t%d\t%s\t%s\n", 'S', $lessonNo, $row->ja, $row->en);
}
usleep(1000 * 1000);
}
}
$courceIds = [
'イディオム' => '5bfc0676-c611-11e8-8186-5f78fef0982a',
'句動詞' => '4d774d54-c611-11e8-9626-a7d3dbc1874c',
];
foreach ($courceIds as $courceId) {
main($courceId);
}