カテゴリー別アーカイブ: programming

[kz] Google Spreadsheetの英文を音声にしてFlashcardsに連携するプログラム

こんにちは、@kazscapeです。

ゴールデンウィークですね。なにしてますか?

私はプログラムなんぞをつくっています。

ゴールデンウィーク前半に作ったプログラムはこちら!

”Google Spreadsheetに書いた英文をGoogle翻訳で音声にして、それをGoogle Driveにアップロードして、暗記帳アプリの「Flashcards Deluxe」に連携する”

がんばって作りましたのでご紹介

Google Spreadsheetの準備

以前の記事でご紹介した単語帳アプリ「Flashcards Deluxe

このアプリ、Google Spreadsheetと連携できるんですね。

「Flashcards Deluxe」とGoogle Driveを連携させるためには、Google Driveに「Flashcards Deluxe」というフォルダを作成して、その配下にSpreadsheetを作成しないといけないこと、さらに音声ファイルは「作成したSpreadsheet名 + <半角スペース> + “Media”」というフォルダに保存しなければならないことに注意してください。

google-drive-for-flashcards-001

今回、「Just Look ‘n Learn English Picture Dictionary」というSpreadsheetを作成したので、「Just Look ‘n Learn English Picture Dictionary Media」というフォルダを作ってあります。

google-drive-for-flashcards-002

Google Spreadsheetを「Flashcards Deluxe」で読み込めるフォーマットは、

  • Text1:いわゆる暗記帳のおもて
  • Text2:いわゆる暗記帳のうら
  • Category1:”Chapter 1″だったり、分類やグループなど
  • Sound 2:音声ファイル

といった項目名を1行目に記載して認識してもらいます

そこで、まずは、Google Spreadsheetをこんなふうに用意します。

google-spreadsheet-for-flashcards-001

Text1には単語を、Text2には英文を。Category1にはグループを入れてあります。Sound2はこのあと作るプログラムで自動で入れてもらうので空欄です。

Googleの認証情報を作ります

今回は、Google Spreadsheetを更新して、Google Driveに音声ファイルを保存するプログラムを作りますが、私以外の誰でも彼でもが好き勝手にそんなことができてはセキュリティずぶずぶですよね。そこで、「私がやってることですよ!登録してるでしょ、私!」的な認証情報を作ります。

まずは、これから作ろうとするプログラムが適切に認証されたプログラムであることを証明するための「認証情報」を取得します。認証情報の取得は「Google API Console」で取得します。

Googleのアカウントをお持ちでなかったら、作ってくださいね。

まずはプロジェクトを作成します。「プロジェクト」なんて、たいそうなものではありませんが。。。

google-api-v4-quickstart-001

自分が作ったプログラムに反応してくれるAPIが有効化されました。続けて「認証情報」を作ります。

google-api-v4-quickstart-002

「プロジェクトへの認証情報の追加」という画面になりますが、一旦、「キャンセル」してください。

google-api-v4-quickstart-003

先に認証方法の「OAuth」の同意をしますので、「OAuth同意画面」のタブを選択してください。

google-api-v4-quickstart-004

「OAuth同意画面」で必要な情報を入力していきます。メールアドレスはGoogleアカウントが自動で表示されます。「サービス名」には適当な名前を入れてください。あとの項目は省略可なので入力してもしなくてもよいです。「保存」ボタンで先に進んでください。

google-api-v4-quickstart-005

「認証情報」のタブに戻るので、ここで「認証情報を作成」に進み、「OAuthクライアントID」を選んでください。

google-api-v4-quickstart-006

今回作ろうとしているプログラムは、表示されている具体的なものとは一致しませんので「その他」を選択。名前は適当な名前を入力して、「作成」ボタンで先に進みます。

google-api-v4-quickstart-007

ここまで進むと「クライアントID」と「クライアントシークレット」という認証情報が取得できます。

google-api-v4-quickstart-008

認証情報は作成しただけでなく、ダウンロードしてください。JSONという形式のファイルがダウンロードできますので、これから作ろうとしているプログラムと同じ場所に保存してください。そして、ファイル名は変更しなくてもいいですが、私は「client_secret.json」というファイル名に変更しておきました。

google-api-v4-quickstart-009

さぁ、プログラミングです。

今回は、最近、勉強している「Python」というプログラミング言語でプログラムを作成してみます。

やりたいことは、

  1. Google Spreadsheetに音声ファイル名を追加すること
  2. Google Spreadsheetに記載されている英文をGoogle翻訳で音声ファイル(MP3)を作成すること
  3. 作成された音声ファイルをGoogle Driveに保存すること

です。

作成したプログラムはこちら。


from __future__ import print_function
import os
import shutil
import httplib2

from googleapiclient import discovery
from googleapiclient.http import MediaFileUpload
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
from gtts import gTTS
from datetime import datetime as dt

SHEET_SCOPES = 'https://www.googleapis.com/auth/spreadsheets'
DRIVE_SCOPES = 'https://www.googleapis.com/auth/drive.file'
SHEET_JSON_NAME = 'sheet2flashcards.json'
DRIVE_JSON_NAME = 'upload2gdrive.json'
SHEET_ID = <SpreadsheetのID>
DRIVE_ID = <Goggle DriveのID
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Google API Python Client'
MAX_COLUMN = 4
TEXT1 = 0
TEXT2 = 1
SOUND2 = 3

try:
    import argparse
    parser = argparse.ArgumentParser(parents=[tools.argparser])
    parser.add_argument('--update',
                        action='store_true',
                        default=False,
                        help='update all contents (default: False)')
    flags = parser.parse_args()
except ImportError:
    flags = None

# 認証
def get_credentials(scopes, json_name):
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir, json_name)

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, scopes)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else: # Needed only for compatibility with Python 2.6
            credentials = tools.run(flow, store)
        print('Storing credentials to ' + credential_path)
    return credentials

# Google SpreadsheetにMP3のファイル名を追加
def append_mp3name_to_gsheet():
    credentials = get_credentials(SHEET_SCOPES, SHEET_JSON_NAME)
    http = credentials.authorize(httplib2.Http())
    discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?'
                    'version=v4')
    service = discovery.build('sheets', 'v4', http=http,
                              discoveryServiceUrl=discoveryUrl)
                    
    result = service.spreadsheets().values().get(
        spreadsheetId=SHEET_ID, range='A2:D').execute()
    values = result.get('values', [])

    if not values:
        print('No data found.')
    else:
        for row in values:
            if len(row) < MAX_COLUMN:
                row.append(row[TEXT1] + '.mp3')

        value_range_body = {"range":"A2:D",
                            "majorDimension":"ROWS",
                            "values":values}
        response = service.spreadsheets().values().update(
                        spreadsheetId=SHEET_ID, range="A2:D",
                        valueInputOption='USER_ENTERED',
                        body=value_range_body).execute()
    return values

# MP3ファイルをGoogle Driveに追加(--updateオプションの場合は総入れ替え)
def mp3_to_gdrive(lists):
    credentials = get_credentials(DRIVE_SCOPES, DRIVE_JSON_NAME)
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('drive', 'v3', http=http)
    
    datetime = dt.now()
    dirname = datetime.strftime('%Y%m%d%H%M%S')
    os.mkdir(dirname)

    query_parents = "'" + DRIVE_ID + "'" + " in parents"

    for row in lists:
        query_mp3name = "name = " + "'" + row[SOUND2] + "'"
        results = service.files().list(
            q = query_parents + " and trashed = False and " +
                query_mp3name, 
            fields = "files(id, name)").execute()
        results = results.get('files', [])

        if not results:
            tts = gTTS(text = row[TEXT2],lang = 'en',slow = False)
            tts.save(os.path.join(dirname, row[SOUND2]))

            file_metadata = {'name':row[SOUND2], 'parents':[DRIVE_ID]}
            media_body = MediaFileUpload(os.path.join(dirname, row[SOUND2]),
                                        mimetype='audio/mp3')
            
            results = service.files().create(body=file_metadata, 
                                    media_body=media_body).execute()
        else:
            if flags.update == True:
                tts = gTTS(text = row[TEXT2], lang = 'en', slow = False)
                tts.save(os.path.join(dirname, row[SOUND2]))

                file_metadata = {'name':row[SOUND2], 'addParents':[DRIVE_ID]}
                media_body = MediaFileUpload(os.path.join(dirname, row[SOUND2]),
                                            mimetype='audio/mp3')
            
                for item in results:
                    file_id = item['id']
                    results = service.files().update(fileId=file_id,
                                        body=file_metadata, 
                                        media_body=media_body).execute()

    shutil.rmtree(dirname)
                                    
def main():
    lists = append_mp3name_to_gsheet()
    mp3_to_gdrive(lists)

if __name__ == '__main__':
    main()

「SHEET_ID」と「DRIVE_ID」はご自身のものに書き換えてください。

「SHEET_ID」は、

google-spreadsheet-for-flashcards-002

「DRIVE_ID」は、

google-drive-for-flashcards-003

それぞれ、URLのところの一部の箇所にあります。

Google SpreadSheetに単語、英文、カテゴリーを入力して、このプログラムを起動すると、音声ファイルを作成してくれて、所定のGoogle Driveに音声ファイルを保存してくれます。

起動方法は

python gtts2flashcards.py

もちろんPythonが動く環境であることが前提です。

「–update」オプションをつけて

python gtts2flashcards.py --update

と起動すると、既存のMP3ファイルも洗い替えてくれるようにしました。

どんなふうな結果になるかというと

Spreadsheetの空だった「Sound 2」の項目に、

google-spreadsheet-for-flashcards-003

音声ファイル名が追加されます。

google-spreadsheet-for-flashcards-004

そして、Mediaのフォルダには、音声ファイルが保存されます。

google-drive-for-flashcards-004

Flashcards Deluxeへの読み込み

ここまでくれば、あとはFlashcards Delexe側で単語帳を読み込むことができます。

単語帳の追加元からGoogle Driveを選択します。

flashcards-deluxe-mp3-download-001

今回作ったSpreadsheetをダウンロードします。

flashcards-deluxe-mp3-download-002

単語を選択すると、

flashcards-deluxe-mp3-download-003

英文が表示されて、その下に再生ボタンが現れます。

flashcards-deluxe-mp3-download-004

再生ボタンを押すと、Google翻訳で作成した音声ファイルが再生されるようになりました。

出来上がりです。

まとめ

ちょっとマニアックな記事でしたでしょうか?

こういうのを作っている時が楽しいんですね。夢中になって作ってました。

これで出先のちょっとした時間でSpreadsheetを更新して、プログラムを動かせば、単語帳を増やせるようになりました。

GoogleのAPIにはこのほかに、カレンダー用のAPIもあるので、次はカレンダーの更新にチャレンジしてみたいと思います。

ゴールデンウィーク前半の成果でした。

ではっ!


[kz] サイボウズ Office 10のスケジュールをical形式にして何につかおう

こんにちは、@kazscapeです。

サイボウズ Office10のスケジュールをical形式にしてみました。

参考にさせていただいたのは「Kung Noi Blog」さんのサイトの「サイボウズOffice10をiCalに変換するスクリプトとCalDavサーバ(ownCloud)にアップロード」の記事と「望遠鏡ドットコム」さんの「サイボウズ9・サイボウズ10のスケジュールをiCal形式でGoogleカレンダーに同期(一方通行)」という記事。

「Kung Noi Blog」さんのサイトにあるPerlのコードでうまくスケジュールを取得できているのですが、スケジュールの時間について、どうもサイボウズの仕様が変わったのか、うまく取得できず、すべての予定が終日の予定になってしまっていました。そこを少し改良させていただいたのと、「望遠鏡ドットコム」さんのところにあったPHPのソースコードから取得できたHTMLから不必要なものを削除する部分を流用させていただきました。

いずれもこの投稿時点のものなので、いつまたサイボウズが仕様を変更するやもしれませんが、投稿してみます。

ソースコード

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use Pod::Usage;
use YAML;
use LWP::UserAgent;
use utf8;
use Encode qw(from_to);
use POSIX qw(mktime strftime);
use Data::Dumper;
my $conf_default = $0;
$conf_default =~ s@/[^/]+$@/config.yaml@;

my %opt = (
   conf => $conf_default,
);
GetOptions( \%opt, 'conf=s', 'help' ) || pod2usage(2);
pod2usage(1) if $opt{help};
my ($cfg) = YAML::LoadFile($opt{conf});
die "Faild to read yaml file: $@" if $@;

my @facility = ();

#my $ua = LWP::UserAgent->new;
my $ua = LWP::UserAgent->new(
                         ssl_opts => {
                             verify_hostname => 0,
                             SSL_verify_mode => 0x00
                            });
my $url = $cfg->{cybozu_url} . "?page=ScheduleUserMonth";
my $req = HTTP::Request->new(POST => $url);
$req->content_type('application/x-www-form-urlencoded');
my $post = '_System=login&_Login=1&LoginMethod=1&_ID='
    . $cfg->{userid} . '&Password=' . $cfg->{password};
$req->content($post);
my $res = $ua->request($req);
die "Failed to access ", $res->status_line , "url=", $url, "\n" unless $res->is_success;
my $content = $res->content;
from_to($content, $cfg->{input_encoding} || 'shiftjis', 'utf8');
$content = Encode::decode('utf-8', $content);
$content = substr($content,index($content,'<table class="schedule usermonth" id="schedulemonth" width="100%" >'));
$content = substr($content,0,index($content,'<table class="monthNavi" '));
$content = substr($content,index($content,'<tbody id="um__body">'));
$content = substr($content,0,index($content,'</table>'));
my @event = split(/(<span class="eventDateTime">.*?<\/span><span class="eventDetail"><a class="event"[^>]*>)/, $content);
if ($cfg->{output_file}) {
    open(OUTPUT, ">$cfg->{output_file}~") || die;
} else {
    open(OUTPUT, ">&STDOUT");
}
binmode OUTPUT, ":utf8";
select(OUTPUT);
print <<EOF;
BEGIN:VCALENDAR
PRODID:$cfg->{calname}
VERSION:2.0
METHOD:PUBLISH
CALSCALE:GREGORIAN
X-WR-CALNAME:$cfg->{calname}
X-WR-CALDESC:$cfg->{calname}
X-WR-TIMEZONE:$cfg->{time_zone}
EOF
for (@event) {
    if (/<span class="eventDateTime">(.*?)<\/span><span class="eventDetail"><a class="event"\s+href="ag.exe\?([^"]+)"\s+title="([^"]+)"/) {
        &event($cfg->{calname}, $ua, $cfg->{cybozu_url}, $post,
               $1, $2, $3, $cfg->{input_encoding});
    }
}
print <<EOF;
BEGIN:VTIMEZONE
TZID:$cfg->{time_zone}
BEGIN:STANDARD
DTSTART:19700101T000000
TZOFFSETFROM:+0900
TZOFFSETTO:+0900
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
EOF
close(OUTPUT);
if ($cfg->{output_file}) {
    rename "$cfg->{output_file}~", $cfg->{output_file};
}

my %cont;
my %recur;

sub event {
    my ($cal, $ua, $url, $post, $time, $query, $title, $encoding) = @_;
    my $eid;
    my $dtstart;
    my $dtend;
    my $description = "";
    my $location;
    my @start;
    my @end;
    $query=~ s/\&amp\;/&/g;

    for (split(/&/, $query)) {
        if (/^BDate=da\.(\d+)\.(\d+)\.(\d+)$/) {
            @start = ($3, $2-1, $1-1900);
        } elsif (/^Date=da\.(\d+)\.(\d+)\.(\d+)$/) {
            @end = ($3, $2-1, $1-1900);
        } elsif (/^sEID=(\d+)$/) {
            $eid = $1;
        }
    }
    if ($time =~ /^\s*(\d+):(\d+)-(\d+):(\d+)&nbsp;/) {
        $dtstart = strftime "DTSTART:%Y%m%dT%H%M%S", 0, $2, $1, @start;
        $dtend = strftime "DTEND:%Y%m%dT%H%M%S", 0, $4, $3, @end;
    } elsif ($time =~ /^\s*(\d+):(\d+)-(\d+)\/(\d+)&nbsp;/) {
        $cont{$eid} = strftime "DTSTART:%Y%m%dT%H%M%S", 0, $2, $1, @start;
        return;
    } elsif ($time =~ /^\s*(\d+)\/(\d+)-(\d+):(\d+)&nbsp;/) {
        $dtend = strftime "DTEND:%Y%m%dT%H%M%S", 0, $4, $3, @end;
        if (defined $cont{$eid}) {
            $dtstart = $cont{$eid};
            delete $cont{$eid};
        } else {
            $dtstart = strftime "DTSTART;VALUE=DATE:%Y%m%d", 0, 0, 0, @start;
        }
    } elsif ($time =~ /^\s*(\d+)\/(\d+)-(\d+)\/(\d+)&nbsp;/) {
        if (defined $cont{$eid}) {
            return;
        }
        $dtstart = strftime "DTSTART;VALUE=DATE:%Y%m%d", 0, 0, 0, @start;
        $dtend = strftime "DTEND;VALUE=DATE:%Y%m%d", 0, 0, 0, @end;
    } else {
        $dtstart = strftime "DTSTART;VALUE=DATE:%Y%m%d", 0, 0, 0, @start;
        $dtend = strftime "DTEND;VALUE=DATE:%Y%m%d", 0, 0, 0, @end;
    }
    my $diff = mktime(0, 0, 0, @start) + 86400 - time();
    if (0 <= $diff && $diff < 604800) {
        my $req = HTTP::Request->new(POST => $url . '?' . $query);
        $req->content_type('application/x-www-form-urlencoded');
        $req->content($post);
        my $res = $ua->request($req);
        if ($res->is_success) {
            my $i = index $res->content, '<a name="ScheduleData">';
            if ($i > 0) {
                my $content = substr($res->content, $i);
                from_to($content, $encoding, 'utf8');
                $content = Encode::decode('utf8', $content);
                for (split(/<th align="left"[^>]*>/, $content)) {
                    if (/^メモ<\/th>/) {
                        my $i = index($_, '<td>') + 4;
                        my $j = index($_, '</td>');
                        $description = substr($_, $i, $j-$i);
                        $description =~ s/[\r\n]+\s*//g;
                        $description =~ s/<br>/\\n\n /g;
                        $description =~ s/<[^>]+>//g;
                    }
                    if (/^設備<\/th>/) {
                        my $i = index($_, '<td>') + 4;
                        my $j = index($_, '</td>');
                        my $facility = substr($_, $i, $j-$i);
                        foreach my $f (@facility) {
                            if (index($facility, $f) >= 0) {
                                $location = $f;
                                last;
                            }
                        }
                    }
                }
            }
        }
    }
    my $uid = "$cal-$eid";
    if (defined $recur{$eid}) {
        $uid .= "-" . $recur{$eid};
        $recur{$eid}++;
    } else {
        $recur{$eid} = 1;
    }
    print <<EOF;
BEGIN:VEVENT
UID:$uid
DESCRIPTION:$description
$dtstart
$dtend
SUMMARY:$title
EOF
    print "LOCATION:$location\n" if defined $location;
    print <<EOF;
END:VEVENT
EOF
}

1;
__END__

=head1 NAME

cybozu10_ical - Convert Cybozu Office8 calendar into iCalendar format

=head1 SYNOPSIS

  % cybozu10_ical
  % cybozu10_ical --conf /path/to/config.yaml

=head1 DESCRIPTION

C<cybozu10_ical> is a command line application that fetches calendar
items from Cybozu Office 8, and converts them into an
iCalendar file.

変更したのは、41行目から45行目にかけて、取得できたHTMLから予定に関わる部分以外のところを削除してしまうようにしています。

これがなくても動くと思うのですが、スケジュールとして、何を取り出せばいいかに集中できるようにしたかったので残してあります。

次の変更箇所は、65行目のif文の条件です。

予定に関わる内容を1行にしているのですが、以前までの条件ではスケジュールの時間が取得できなくなっていたので、<span class=”eventDateTime”>のタグからスケジュールの時間を追加で取得するように条件を変更しました。そのため、関数の”event”に与える引数が一つ増えてしまったので$3まで増やしました。

89行目からの関数”event”では、引数として$timeを追加して、109行めから132行目までのif文で使用している変数を全て$timeにして、$titleには何も手を加えないようにしています。

「Kung Noi Blog」さんのところのソースコードと見比べないと何のこっちゃかもしれませんが、詳しくみたい方は両方をdiffなどしていただけると。

実行方法は元のままです

実行方法は、「Kung Noi Blog」さんのところで書かれていらっしゃるとおり、設定ファイルは「getcybozu10.yaml」の名前で以下のとおりご用意いただいて、

cybozu_url: https://fogefoge.com/cgi-bin/cbag/ag.cgi
calname: Cybozu
userid: 1
password: foge
time_zone: Asia/Tokyo
input_encoding: utf-8
output_file: /tmp/cybozu10ical.ics

実行は、それぞれが保存している場所でお願いします。

perl getcybozu10.pl –conf getcybozu10.yaml

まとめ

先駆者の方々の有益な資産を利用させていただき、少しばかり改良させていただいて、目的のical形式のファイルを取得することができました。

大変ありがとうございます。

さて、このical形式のファイル、何に使おう。。。

ではっ!