はてなダイアリーライター(略称:はてダラ)

テキストファイルを自動書き込み

結城浩

はてなダイアリーライター(はてダラ)は、 ローカルに作った 2004-08-19.txt のようなテキストファイルを、 はてなダイアリーの日記として自動書き込みするコマンドラインツールです。
ご連絡: (2009-09-12) スクリプトをGithubで管理はじめました。(hatena-diary-writer)
ご連絡: (2009-08-04) はてダラがhttpsなページでうまく動かない

目次

詳細目次

はじめに

はてなダイアリーは、Webブラウザから書き込みができ、手軽で便利な日記(ブログ)サービスです。

でも、以下のような場合には、Webブラウザでいちいち書き込みをするのは面倒です。

そこで、 ローカルに作った「2004-08-19.txt」のようなテキストファイルの内容を、 はてなダイアリーの「2004年8月19日の日記」として書き込むPerlスクリプトを書きました。

それが「はてなダイアリーライター」(略称:はてダラ)です。

テキストファイルのタイムスタンプを自動判定し、 新しく作ったファイル、 再編集したファイルだけをはてなダイアリーに送信します。

「はてダラ」はフリーソフトウェアです。 バグなどのご報告は大歓迎します。 ライセンスはPerlと同じです。

なお、このツール「はてダラ」は結城が独自に作成したものです。 株式会社はてなへの問い合わせはご遠慮ください。

インストールと基本的な使い方

「はてダラ」を動かすのに必要なもの

環境設定

日記を書いて送信する

日記をテキストファイルとして書きます。 ファイル名は 2004-08-19.txt のように、YYYY-MM-DD.txtの形式にします。このファイルがYYYY年MM月DD日の日記になります。

日記ファイルの内容は、次のように書いてください。

以下に例を示します。以下の例で「今日は楽しかった」の部分がタイトルになり、「Perlでプログラム」が小見出しになります。

今日は楽しかった
*Perlでプログラム
今日は思う存分Perlでプログラムを書いた。とても楽しい一日だった。

日記ファイルの準備ができたら、 コマンドラインから次のように入力します。

perl hw.pl

すると、次のように表示されます。画面の指示に合わせてユーザIDとパスワードを入力してください。 自動的にはてなダイアリーのサイトに接続して、日記の内容を送信します。

Username: xxxx
Password: xxxx
Login to Hatena as xxxx.
Login OK.
Post 2004-08-19 : 今日は楽しかった
Post OK.
Logout from Hatena as xxxx.
Logout OK.

送信ごとに、ファイル名と見出しを表示しているのがわかると思います。

チェック対象となるのは、 カレントディレクトリの中にある、 YYYY-MM-DD.txtの形式のファイルです。 前回送信してから編集されたファイルはすべて送信されます。 一度送信したものは再編集しない限り(つまりタイムスタンプが新しくなっていない限り)再送信はしません。

送信するファイルがない場合には、次のように表示されます。

No files are posted.

メモ: 上記に示した方法で使い続ける場合には、 日記ファイルの文字コードはEUC-JPで保存したほうが文字化けが起きにくいです。 Shift_JISなど他の文字コードを使う場合には、 設定ファイルでserver_encoding, client_encodingを指定するとよいかもしれません。

その日の日記を削除する

ファイルの一行目(つまり、その日のタイトル)にdeleteと書くと、 その日の日記を削除します。二行目以降は残してもかまいません。 以下に例を示します(一行目がdeleteになっていることに注目)。

delete
今日は悲しかった
*プログラムが書けない
今日はばたばたしていてプログラムが書けなかった。とても悲しい一日だった。
この日記はあとで消しちゃうかも。

上記のファイルが2004-08-18.txtだとすると、 perl hw.plの実行結果は次のようになります。

Username: xxxx
Password: xxxx
Login to Hatena as xxxx.
Login OK.
Delete 2004-08-18.
Delete OK.
Logout from Hatena as xxxx.
Logout OK.

削除されるのは、はてなダイアリーのサーバ上にあるその日の日記です。 ローカルにあるファイルそのもの、上で言えばファイル2004-08-18.txtはそのまま残ります。

「はてダラ」が日記ファイルの内容を書き換えることは基本的にありません。 しかし、 *t*の形式の小見出しを含んでいる場合は例外 です。 「*t*で始まっている行」があると、その部分は *タイムスタンプ* に置換されます。 これがいやな方は、 -M オプションをつけると書き換えられなくなります。

その日の画像を送信する

「はてダラ」では日記用の画像を送信することもできます。

たとえば、日記ファイル2004-08-24.txtと同じディレクトリに2004-08-24.jpgというファイルがあると、 その画像ファイルも自動的に送信されます。

詳しくは、 FAQ:画像ファイルを送信したいを参照してください。

さらに使いやすくするには

「はてダラ」の起動オプションや設定ファイルを用いて、 あなたのスタイルに合った利用法を探してみてください。 以下のドキュメントが参考になります。

あ、それから、もし「はてダラ」を気に入っていただけましたら、 ぜひ、 作者へフィードバックをお送りください。

「はてダラ」スクリプト本体のダウンロード

注意: このスクリプトは無保証です。あなたの自己責任でご利用ください。

#!/usr/bin/perl
#
# hw.pl - Hatena Diary Writer.
#
# Copyright (C) 2004,2005,2007 by Hiroshi Yuki.
# <hyuki@hyuki.com>
# http://www.hyuki.com/techinfo/hatena_diary_writer.html
#
# Special thanks to:
# - Ryosuke Nanba http://d.hatena.ne.jp/rna/
# - Hahahaha http://www20.big.or.jp/~rin_ne/
# - Ishinao http://ishinao.net/
#
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
use strict;
my $VERSION = "1.4.1";

use LWP::UserAgent;
use HTTP::Request::Common;
use HTTP::Cookies;
use File::Basename;
use Getopt::Std;
use Digest::MD5 qw(md5_base64);

my $enable_encode = eval('use Encode; 1');

# Prototypes.
sub login();
sub get_rkm($$$$$$$$$$$);
sub logout();
sub update_diary_entry($$$$$$);
sub delete_diary_entry($);
sub doit_and_retry($$);
sub create_it($$$);
sub delete_it($);
sub post_it($$$$$$);
sub get_timestamp();
sub print_debug(@);
sub print_message(@);
sub read_title_body($);
sub find_image_file($);
sub replace_timestamp($);
sub error_exit(@);
sub load_config();

# Hatena user id (if empty, I will ask you later).
my $username = '';
# Hatena password (if empty, I will ask you later).
my $password = '';
# Hatena group name (for hatena group user only).
my $groupname = '';

# Default file names.
my $touch_file = 'touch.txt';
my $cookie_file = 'cookie.txt';
my $config_file = 'config.txt';
my $target_file = '';

# Filter command.
# e.g. 'iconv -f euc-jp -t utf-8 %s'
# where %s is filename, output is stdout.
my $filter_command = '';

# Proxy setting.
my $http_proxy = '';

# Directory for "YYYY-MM-DD.txt".
my $txt_dir = ".";

# Client and server encodings.
my $client_encoding = '';
my $server_encoding = '';

# Hatena URL.
my $hatena_url = 'http://d.hatena.ne.jp';
my $hatena_sslregister_url = 'https://www.hatena.ne.jp/login';

# Crypt::SSLeay check.
eval {
    require Crypt::SSLeay;
};
if ($@) {
    print_message("WARNING: Crypt::SSLeay is not found, use non-encrypted HTTP mode.");
    $hatena_sslregister_url = 'http://www.hatena.ne.jp/login';
}

# Option for LWP::UserAgent.
my %ua_option = (
    agent => "HatenaDiaryWriter/$VERSION", # "Mozilla/5.0",
    timeout => 180,
);

# Other variables.
my $delete_title = 'delete';
my $cookie_jar;
my $user_agent;
my $rkm; # session id for posting.

# Handle command-line option.
my %cmd_opt = (
    'd' => 0,   # "debug" flag.
    't' => 0,   # "trivial" flag.
    'u' => "",  # "username" option.
    'p' => "",  # "password" option.
    'a' => "",  # "agent" option.
    'T' => "",  # "timeout" option.
    'c' => 0,   # "cookie" flag.
    'g' => "",  # "groupname" option.
    'f' => "",  # "file" option.
    'M' => 0,   # "no timestamp" flag.
    'n' => "",  # "config file" option.
    'S' => 1,   # "SSL" option. This is always 1. Set 0 to login older hatena server.
);

$Getopt::Std::STANDARD_HELP_VERSION = 1;
getopts("tdu:p:a:T:cg:f:Mn:", \%cmd_opt) or error_exit("Unknown option.");

if ($cmd_opt{d}) {
    print_debug("Debug flag on.");
    print_debug("Cookie flag on.") if $cmd_opt{c};
    print_debug("Trivial flag on.") if $cmd_opt{t};
    &VERSION_MESSAGE();
}

# Override config file name (before load_config).
$config_file = $cmd_opt{n} if $cmd_opt{n};

# Override global vars with config file.
load_config() if -e($config_file);

# Override global vars with command-line options.
$username = $cmd_opt{u} if $cmd_opt{u};
$password = $cmd_opt{p} if $cmd_opt{p};
$groupname = $cmd_opt{g} if $cmd_opt{g};
$ua_option{agent} = $cmd_opt{a} if $cmd_opt{a};
$ua_option{timeout} = $cmd_opt{T} if $cmd_opt{T};
$target_file = $cmd_opt{f} if $cmd_opt{f};

# Change $hatena_url to Hatena group URL if ($groupname is defined).
if ($groupname) {
    $hatena_url = "http://$groupname.g.hatena.ne.jp";
}

# Start.
&main;

# no-error exit.
exit(0);

# Main sequence.
sub main {
    my $count = 0;
    my @files;

    # Setup file list.
    if ($cmd_opt{f}) {
        # Do not check timestamp.
        push(@files, $cmd_opt{f});
        print_debug("main: files: option -f: @files");
    } else {
        while (glob("$txt_dir/*.txt")) {
            # Check timestamp.
            next if (-e($touch_file) and (-M($_) > -M($touch_file)));
            push(@files, $_);
        }
        print_debug("main: files: current dir ($txt_dir): @files");
    }

    # Process it.
    for (@files) {
        # Check file name.
        next unless (/\b(\d\d\d\d)-(\d\d)-(\d\d)\.txt$/);

        my ($year, $month, $day) = ($1, $2, $3);
        my $date = $year . $month . $day;

        # Check if it is a file.
        next unless (-f $_);

        # Login if necessary.
        login() unless ($user_agent);

        # Replace "*t*" unless suppressed.
        replace_timestamp($_) unless ($cmd_opt{M});

        # Read title and body.
        my ($title, $body) = read_title_body($_);

        # Find image files.
        my $imgfile = find_image_file($_);

        if ($title eq $delete_title) {
            # Delete entry.
            print_message("Delete $year-$month-$day.");
            delete_diary_entry($date);
            print_message("Delete OK.");
        } else {
            # Update entry.
            print_message("Post $year-$month-$day.  " . ($imgfile ? " (image: $imgfile)" : ""));
            update_diary_entry($year, $month, $day, $title, $body, $imgfile);
            print_message("Post OK.");
        }

        sleep(1);

        $count++;
    }

    # Logout if necessary.
    logout if ($user_agent);

    if ($count == 0) {
        print_message("No files are posted.");
    } else {
        unless ($cmd_opt{f}) {
            # Touch file.
            open(FILE, "> $touch_file") or die "$!:$touch_file\n";
            print FILE get_timestamp;
            close(FILE);
        }
    }
}

# Login.
sub login() {
    $user_agent = LWP::UserAgent->new(%ua_option);
    $user_agent->env_proxy;
    if ($http_proxy) {
        $user_agent->proxy('http', $http_proxy);
        print_debug("login: proxy for http: $http_proxy");
        $user_agent->proxy('https', $http_proxy);
        print_debug("login: proxy for https: $http_proxy");
    }

    # Ask username if not set.
    unless ($username) {
        print "Username: ";
        chomp($username = <STDIN>);
    }

    # If "cookie" flag is on, and cookie file exists, do not login.
    if ($cmd_opt{c} and -e($cookie_file)) {
        print_debug("login: Loading cookie jar.");

        $cookie_jar = HTTP::Cookies->new;
        $cookie_jar->load($cookie_file);
        $cookie_jar->scan(\&get_rkm);

        print_debug("login: \$cookie_jar = " . $cookie_jar->as_string);

        print_message("Skip login.");

        return;
    }

    # Ask password if not set.
    unless ($password) {
        print "Password: ";
        chomp($password = <STDIN>);
    }

    my %form;
    $form{name} = $username;
    $form{password} = $password;

    my $r; # Response.
    if ($cmd_opt{S}) {
        my $diary_url = "$hatena_url/$username/";

        $form{backurl} = $diary_url;
        $form{mode} = "enter";
        if ($cmd_opt{c}) {
            $form{persistent} = "1";
        }

        print_message("Login to $hatena_sslregister_url as $form{name}.");

        $r = $user_agent->simple_request(
            HTTP::Request::Common::POST("$hatena_sslregister_url", \%form)
        );

        print_debug("login: " . $r->status_line);

        print_debug("login: \$r = " . $r->content());
    } else {
        # For older version.

        print_message("Login to $hatena_url as $form{name}.");
        $r = $user_agent->simple_request(
            HTTP::Request::Common::POST("$hatena_url/login", \%form)
        );

        print_debug("login: " . $r->status_line);

        if (not $r->is_redirect) {
            error_exit("Login: Unexpected response: ", $r->status_line);
        }
    }

    print_message("Login OK.");

    print_debug("login: Making cookie jar.");

    $cookie_jar = HTTP::Cookies->new;
    $cookie_jar->extract_cookies($r);
    $cookie_jar->save($cookie_file);
    $cookie_jar->scan(\&get_rkm);

    print_debug("login: \$cookie_jar = " . $cookie_jar->as_string);
}

# get session id.
sub get_rkm($$$$$$$$$$$) {
    my ($version, $key, $val) = @_;
    if ($key eq 'rk') {
        $rkm = md5_base64($val);
        print_debug("get_rkm: \$rkm = " . $rkm);
    }
}

# Logout.
sub logout() {
    return unless $user_agent;

    # If "cookie" flag is on, and cookie file exists, do not logout.
    if ($cmd_opt{c} and -e($cookie_file)) {
        print_message("Skip logout.");
        return;
    }

    my %form;
    $form{name} = $username;
    $form{password} = $password;

    print_message("Logout from $hatena_url as $form{name}.");

    $user_agent->cookie_jar($cookie_jar);
    my $r = $user_agent->get("$hatena_url/logout");
    print_debug("logout: " . $r->status_line);

    if (not $r->is_redirect and not $r->is_success) {
        error_exit("Logout: Unexpected response: ", $r->status_line);
    }

    unlink($cookie_file);

    print_message("Logout OK.");
}

# Update entry.
sub update_diary_entry($$$$$$) {
    my ($year, $month, $day, $title, $body, $imgfile) = @_;

    if ($cmd_opt{t}) {
        # clear existing entry. if the entry does not exist, it has no effect.
        doit_and_retry("update_diary_entry: CLEAR.", sub { return post_it($year, $month, $day, "", "", "") });
    }

    # Make empty entry before posting.
    doit_and_retry("update_diary_entry: CREATE.", sub { return create_it($year, $month, $day) });

    # Post.
    doit_and_retry("update_diary_entry: POST.", sub { return post_it($year, $month, $day, $title, $body, $imgfile) });
}

# Delete entry.
sub delete_diary_entry($) {
    my ($date) = @_;

    # Delete.
    doit_and_retry("delete_diary_entry: DELETE.", sub { return delete_it($date) });
}

# Do the $funcref, and retry if fail.
sub doit_and_retry($$) {
    my ($msg, $funcref) = @_;
    my $retry = 0;
    my $ok = 0;

    while ($retry < 2) {
        $ok = $funcref->();
        if ($ok or not $cmd_opt{c}) {
            last;
        }
        print_debug("try_it: $msg");
        unlink($cookie_file);
        print_message("Old cookie. Retry login.");
        login();
        $retry++;
    }

    if (not $ok) {
        error_exit("try_it: Check username/password.");
    }
}

# Delete.
sub delete_it($) {
    my ($date) = @_;

    print_debug("delete_it: $date");

    $user_agent->cookie_jar($cookie_jar);

    my $r = $user_agent->simple_request(
        HTTP::Request::Common::POST("$hatena_url/$username/edit",
            # Content_Type => 'form-data',
            Content => [
                mode => "delete",
                date => $date,
                rkm => $rkm,
            ]
        )
    );

    print_debug("delete_it: " . $r->status_line);

    if ((not $r->is_redirect()) and (not $r->is_success())) {
        error_exit("Delete: Unexpected response: ", $r->status_line);
    }

    print_debug("delete_it: Location: " . $r->header("Location"));

    # Check the result. ERROR if the location ends with the date.
    # (Note that delete error != post error)
    if ($r->header("Location") =~ m(/$date$)) {
        print_debug("delete_it: returns 0 (ERROR).");
        return 0;
    } else {
        print_debug("delete_it: returns 1 (OK).");
        return 1;
    }
}

sub create_it($$$) {
    my ($year, $month, $day) = @_;

    print_debug("create_it: $year-$month-$day.");

    $user_agent->cookie_jar($cookie_jar);

    my $r = $user_agent->simple_request(
        HTTP::Request::Common::POST("$hatena_url/$username/edit",
            Content_Type => 'form-data',
            Content => [
                mode => "enter",
                timestamp => get_timestamp,
                year => $year,
                month => $month,
                day => $day,
                trivial => $cmd_opt{t},
                rkm => $rkm,

                # Important:
                # If (entry does exists) { append empty string (i.e. nop) }
                # If (entry does not exist) { create empty entry }
                title => "",
                body => "",
                date => "",
            ]
        )
    );

    print_debug("create_it: " . $r->status_line);

    if ((not $r->is_redirect()) and (not $r->is_success())) {
        error_exit("Create: Unexpected response: ", $r->status_line);
    }

    print_debug("create_it: Location: " . $r->header("Location"));

    # Check the result. OK if the location ends with the date.
    if ($r->header("Location") =~ m(/$year$month$day$)) {
        print_debug("create_it: returns 1 (OK).");
        return 1;
    } else {
        print_debug("create_it: returns 0 (ERROR).");

        return 0;
    }
}

sub post_it($$$$$$) {
    my ($year, $month, $day, $title, $body, $imgfile) = @_;

    print_debug("post_it: $year-$month-$day.");

    $user_agent->cookie_jar($cookie_jar);

    my $r = $user_agent->simple_request(
        HTTP::Request::Common::POST("$hatena_url/$username/edit",
            Content_Type => 'form-data',
            Content => [
                mode => "enter",
                timestamp => get_timestamp,
                year => $year,
                month => $month,
                day => $day,
                title => $title,
                trivial => $cmd_opt{t},
                rkm => $rkm,

                # Important:
                # This entry must already exist.
                body => $body,
                date => "$year$month$day",
                image => [
                    $imgfile,
                ]
            ]
        )
    );

    print_debug("post_it: " . $r->status_line);

    if (not $r->is_redirect) {
        error_exit("Post: Unexpected response: ", $r->status_line);
    }

    print_debug("post_it: Location: " . $r->header("Location"));

    # Check the result. OK if the location ends with the date.
    if ($r->header("Location") =~ m(/$year$month$day$)) {
        print_debug("post_it: returns 1 (OK).");
        return 1;
    } else {
        print_debug("post_it: returns 0 (ERROR).");
        return 0;
    }
}

# Get "YYYYMMDDhhmmss" for now.
sub get_timestamp() {
    my (@week) = qw(Sun Mon Tue Wed Thu Fri Sat);
    my ($sec, $min, $hour, $day, $mon, $year, $weekday) = localtime(time);
    $year += 1900;
    $mon++;
    $mon = "0$mon" if $mon < 10;
    $day = "0$day" if $day < 10;
    $hour = "0$hour" if $hour < 10;
    $min = "0$min" if $min < 10;
    $sec = "0$sec" if $sec < 10;
    $weekday = $week[$weekday];
    return "$year$mon$day$hour$min$sec";
}

# Show version message. This is called by getopts.
sub VERSION_MESSAGE {
    print <<"EOD";
Hatena Diary Writer Version $VERSION
Copyright (C) 2004,2005 by Hiroshi Yuki.
EOD
}

# Debug print.
sub print_debug(@) {
    if ($cmd_opt{d}) {
        print "DEBUG: ", @_, "\n";
    }
}

# Print message.
sub print_message(@) {
    print @_, "\n";
}

# Error exit.
sub error_exit(@) {
    print "ERROR: ", @_, "\n";
    unlink($cookie_file);
    exit(1);
}

# Read title and body.
sub read_title_body($) {
    my ($file) = @_;

    # Execute filter command, if any.
    my $input = $file;
    if ($filter_command) {
        $input = sprintf("$filter_command |", $file);
    }
    print_debug("read_title_body: input: $input");
    if (not open(FILE, $input)) {
        error_exit("$!:$input");
    }
    my $title = <FILE>; # first line.
    chomp($title);
    my $body = join('', <FILE>); # rest of all.
    close(FILE);

    # Convert encodings.
    if ($enable_encode and ($client_encoding ne $server_encoding)) {
        print_debug("Convert from $client_encoding to $server_encoding.");
        Encode::from_to($title, $client_encoding, $server_encoding);
        Encode::from_to($body, $client_encoding, $server_encoding);
    }

    return($title, $body);
}

# Find image file.
sub find_image_file($) {
    my ($fulltxt) = @_;
    my ($base, $path, $type) = fileparse($fulltxt, qr/\.txt/);
    for my $ext ('jpg', 'png', 'gif') {
        my $imgfile = "$path$base.$ext";
        if (-e $imgfile) {
            if ($cmd_opt{f}) {
                print_debug("find_image_file: -f option, always update: $imgfile");
                return $imgfile;
            } elsif (-e($touch_file) and (-M($imgfile) > -M($touch_file))) {
                print_debug("find_image_file: skip $imgfile (not updated).");
                next;
            } else {
                print_debug("find_image_file: $imgfile");
                return $imgfile;
            }
        }
    }
    return undef;
}

# Replace "*t*" with timestamp.
sub replace_timestamp($) {
    my ($filename) = @_;

    # Read.
    open(FILE, $filename) or error_exit("$!: $filename");
    my $file = join('', <FILE>);
    close(FILE);

    # Replace.
    my $newfile = $file;
    $newfile =~ s/^\*t\*/"*" . time() . "*"/gem;

    # Write if replaced.
    if ($newfile ne $file) {
        print_debug("replace_timestamp: $filename");
        open(FILE, "> $filename") or error_exit("$!: $filename");
        print FILE $newfile;
        close(FILE);
    }
}

# Show help message. This is called by getopts.
sub HELP_MESSAGE {
    print <<"EOD";

Usage: perl $0 [Options]

Options:
    --version       Show version.
    --help          Show this message.
    -t              Trivial. Use this switch for trivial edit (i.e. typo).
    -d              Debug. Use this switch for verbose log.
    -u username     Username. Specify username.
    -p password     Password. Specify password.
    -a agent        User agent. Default value is HatenaDiaryWriter/$VERSION.
    -T seconds      Timeout. Default value is 180.
    -c              Cookie. Skip login/logout if $cookie_file exists.
    -g groupname    Groupname. Specify groupname.
    -f filename     File. Send only this file without checking timestamp.
    -M              Do NOT replace *t* with current time.
    -n config_file  Config file. Default value is $config_file.

Config file example:
#
# $config_file
#
id:yourid
password:yourpassword
cookie:cookie.txt
# txt_dir:/usr/yourid/diary
# touch:/usr/yourid/diary/hw.touch
# proxy:http://www.example.com:8080/
# g:yourgroup
# client_encoding:Shift_JIS
# server_encoding:UTF-8
## for Unix, if Encode module is not available.
# filter:iconv -f euc-jp -t utf-8 %s
EOD
}

# Load config file.
sub load_config() {
    print_debug("Loading config file ($config_file).");
    if (not open(CONF, $config_file)) {
        error_exit("Can't open $config_file.");
    }
    while (<CONF>) {
        chomp;
        if (/^\#/) {
            # skip comment.
        } elsif (/^$/) {
            # skip blank line.
        } elsif (/^id:([^:]+)$/) {
            $username = $1;
            print_debug("load_config: id:$username");
        } elsif (/^g:([^:]+)$/) {
            $groupname = $1;
            print_debug("load_config: g:$groupname");
        } elsif (/^password:(.*)$/) {
            $password = $1;
            print_debug("load_config: password:********");
        } elsif (/^cookie:(.*)$/) {
            $cookie_file = glob($1);
            $cmd_opt{c} = 1; # If cookie file is specified, Assume '-c' is given.
            print_debug("load_config: cookie:$cookie_file");
        } elsif (/^proxy:(.*)$/) {
            $http_proxy = $1;
            print_debug("load_config: proxy:$http_proxy");
        } elsif (/^client_encoding:(.*)$/) {
            $client_encoding = $1;
            print_debug("load_config: client_encoding:$client_encoding");
        } elsif (/^server_encoding:(.*)$/) {
            $server_encoding = $1;
            print_debug("load_config: server_encoding:$server_encoding");
        } elsif (/^filter:(.*)$/) {
            $filter_command = $1;
            print_debug("load_config: filter:$filter_command");
        } elsif (/^txt_dir:(.*)$/) {
            $txt_dir = glob($1);
            print_debug("load_config: txt_dir:$txt_dir");
        } elsif (/^touch:(.*)$/) {
            $touch_file = glob($1);
            print_debug("load_config: touch:$touch_file");
        } else {
            error_exit("Unknown command '$_' in $config_file.");
        }
    }
    close(CONF);
}
__END__

コマンドラインオプション

「はてダラ」(hw.pl)をより便利に使うためにコマンドラインオプションがいくつか用意されています。

ユーザ名指定オプション(-u)

-u ユーザ名 というオプションをつけるとユーザ名を指定することができます。 変数$usernameの内容は無視されます(オプションが優先)。

perl hw.pl -u hyuki

パスワード指定オプション(-p)

-p パスワード というオプションをつけるとパスワードを指定することができます。 普通は-uオプションと一緒に使います。 変数$passwordの内容は無視されます(オプションが優先)。 当然ながら、パスワードの管理にはご注意ください。

perl hw.pl -u hyuki -p xxxxxxxx

ちょっとした更新(-t)

誤字の訂正など「ちょっとした更新」の場合には -t というオプションをつけて、以下のように実行してください。 これは、はてなダイアリーの編集画面で「ちょっとした更新」のチェックボックスをオンにした効果があります。

perl hw.pl -t

デバッグ表示(-d)

何だか動きがおかしいときには、 -d というオプションをつけて実行します。 すると、ちょっぴり詳しい情報が表示されます。

perl hw.pl -d

たとえば次のようになります (これは$usernameと$passwordを空にしていた場合の例です)。

DEBUG: Debug flag on.
Hatena Diary Writer Version X.X.X
Copyright (C) 2004 by Hiroshi Yuki.
Username: xxxx
Password: xxxx
Login to Hatena as ....
DEBUG: login: 302 Moved
Login OK.
DEBUG: login: Making cookie jar.
DEBUG: login: $cookie_jar = Set-Cookie3: rk=7264f...
Post 2004-08-XX :
DEBUG: create_it: 2004-08-XX.
DEBUG: create_it: 302 Moved
DEBUG: create_it: Location: http://d.hatena.ne.jp...
DEBUG: create_it: returns 1 (OK).
DEBUG: post_it: 2004-08-XX.
DEBUG: post_it: 302 Moved
DEBUG: post_it: Location: http://d.hatena.ne.jp...
DEBUG: post_it: returns 1 (OK).
Post OK.
Logout from Hatena as ....
DEBUG: logout: 200 OK
Logout OK.

ヘルプメッセージ表示(--help)

--help というオプションをつけるとヘルプメッセージが表示されます。 コマンドラインオプションを調べるときに使います。

perl hw.pl --help

バージョン表示(--version)

--version というオプションをつけるとバージョン番号が表示されます。

perl hw.pl --version

ユーザエージェントの指定(-a)

-a ユーザエージェント名 というオプションでユーザエージェントを指定できます。デフォルトはHatenaDiaryWriter/X.X.Xです。

perl hw.pl -a Mozilla/5.0

タイムアウト時間の指定(-T)

-T タイムアウト でタイムアウトの時間(秒)を指定できます。デフォルトは180です。

perl hw.pl -T 30

クッキーの利用(ログイン・ログアウトの省略)(-c)

-c オプションでクッキーファイルを利用します。

クッキーファイルがあるときにはログイン/ログアウトをスキップしますので、速度がちょっと速くなります。

ブラウザと並行して「はてダラ」を使っている場合に、ブラウザ側が強制的にログアウトしてしまう可能性を減らします。

ただし、-cオプションを使えば絶対にブラウザ側がログアウトしなくなるわけではありません。

また、-cオプションをつけているときにエラーになった場合(クッキーが古かった場合)には再ログインを行います。 その際にユーザ名とパスワードを再度問い合わせる場合があります。

このオプションを使う際には-uオプションを併用することをお勧めします。

perl hw.pl -c -u hyuki

送信ファイルの指定(-f)

-f ファイル名 で指定したファイルを送信します。 このファイルは、別のディレクトリにあってもかまいませんが、YYYY-MM-DD.txtの形式である必要があります。 また、-fオプションを使った場合にはタイムスタンプは無視され、常に送信が行われます。 また、この場合にはtouch.txtのタイムスタンプは更新されません。

perl hw.pl -f ../tmp/2004-08-24.txt

見出しタイムスタンプの置換禁止(-M)

-M で、見出しタイムスタンプの置換を禁止します。

「はてダラ」はデフォルトで、日記ファイルの「 *t* 」で始まる行を「 *1093376729* 」のように現在時刻に置換します。 これは、はてなダイアリーの見出しタイムスタンプ機能を模倣したものです。

日記ファイルを書き換えられるのがいやな場合には、 -M オプションをつけます。 そうすると日記ファイルの置換は行われません。

perl hw.pl -M

設定ファイルの指定(-n)

-n 設定ファイル で、設定ファイルを指定します。デフォルトはconfig.txtです。

perl hw.pl -n hw.conf

設定ファイル

「はてダラ」では、カレントディレクトリに「設定ファイル」があると その内容を読み込んで振る舞いを変更します。 設定ファイルはデフォルトではconfig.txtという名前ですが、-nオプションで変更することもできます。

設定ファイルを使うと、

を置いておくディレクトリを使い分けながら、 クッキーファイルを両者で共有することができます。

また、はてなグループの日記では文字コードがUTF-8ですが、 その変換を行うための好みのフィルタを指定することも可能です。

設定ファイルの機能はなんばさんからのパッチ情報によるものです。 なんばさん、ありがとうございます。

設定ファイルの例(1)

以下のようにconfig.txtを作成すると、 ユーザ名がhyuki、パスワードがxxxxxxxx、そしてクッキーファイルがcookie.txtになります。 クッキーファイルを指定すると、自動的に-cオプションが指定されたものとみなされます。

# 設定ファイル例
id:hyuki
password:xxxxxxxx
cookie:cookie.txt
client_encoding:Shift_JIS
server_encoding:EUC-JP

また、上では、日記ファイルをShift_JISで作っておき、 送信する際にEUC-JPに変換する指定もしてあります。 ただし、 これが本当に有効になるのはEncodeモジュールが使用できるときだけです。 もしも日記ファイルをEUC-JPで作る場合には、

client_encoding:Shift_JIS
server_encoding:EUC-JP

は削除してください。

設定ファイルの例(2)

以下は、UNIXの場合のconfig.txtを作成例です。

#
# config.txt 設定ファイル例(UNIX)
#
id:hyuki
password:xxxxxxxx
cookie:~/hw/cookie.txt
filter:iconv -f euc-jp -t utf-8 %s
g:mygroup

このように設定すると、 ユーザ名がhyuki、パスワードがxxxxxxxx、そしてクッキーファイルが~/hw/cookie.txtになります。 UNIXの場合、~ はホームディレクトリに置換されます。

また、日記ファイルを読み込むときにiconv -f euc-jp -t utf-8 %sというフィルタを通します(iconvというのはUNIXのコマンドです)。 filter:の後に指定するのは、%sにファイル名を与えられると標準出力にフィルタ結果を出力するコマンドです。

また、g:グループ名で日記を更新するグループ名を指定します。

FAQ:はてなグループの日記更新に使いたいも参考にどうぞ。

設定ファイルで使えるコマンド一覧

#
# はてなのユーザID(デフォルトは空)
#
id:yourid
#
# パスワード(デフォルトは空)
#
password:yourpassword
#
# クッキーファイル(デフォルトは cookie.txt)
#
cookie:cookie.txt
#
# グループ名(はてなグループの日記更新のときのみ設定)(デフォルトは空)
#
g:yourgroup
#
# ディレクトリ:YYYY-MM-DD.txtのファイルを置いておく場所(デフォルトは . )
#
txt_dir:/usr/yourid/diary
#
# タッチファイル:送信時に更新されるファイル(デフォルトは touch.txt)
#
touch:/usr/yourid/diary/hw.touch
#
# HTTPプロキシー(デフォルトは空)
proxy:http://www.example.com:8080/
#
# クライアント(ローカル)側の文字コード(デフォルトは空)
#
# client_encoding:EUC-JP
#
client_encoding:Shift_JIS
#
# サーバ(はてな)側の文字コード(デフォルトは空)
#
server_encoding:UTF-8
#
# フィルタコマンド
#
filter:iconv -f euc-jp -t utf-8 %s

ちょっとしたコツ

ユーザ名やパスワードの固定

いちいちユーザ名やパスワードをキーボードから入力するのが面倒なときには、 設定ファイルconfig.txtに埋め込んでしまうのが便利です。 たとえば、カレントディレクトリに次のようなconfig.txtファイルを作ります。

# 設定ファイル
id:hyuki
password:xxxxxxxxx

パスワードを書くことに抵抗がある人は、ユーザIDだけを埋め込むという手もあります。

# 設定ファイル
id:hyuki

この場合には、パスワードだけをキーボードから入力することになります。

バッチファイルを作る

Windowsの場合、次のようにするとhw.batというバッチファイルを作ることができます。

pl2bat hw.pl

これ以降は、perl hw.plではなく、単にhwと入力するだけで「はてなダイアリーライター」を動かすことができます。 hw.plを書き換えた場合にはpl2batを再度動かすのを忘れないようにしてください。

自分専用のバッチファイルを作る

「はてダラ」を何回か試して、自分の利用形態にマッチしたオプションが確定したら、 自分専用のバッチファイルを作ると便利です。 たとえば、以下のようなバッチファイル(myhw.bat)を作ったとします。

perl hw.pl -u hyuki -p xxxxxxxx -c %1 %2 %3 %4 %5 %6 %7 %8 %9

これで、

myhw

のようにバッチファイルを動かすと、

という設定で実行されることになります。 また%1〜%9でコマンドライン引数を取り入れるようになっていますので、 「ちょっとした更新」をしたいときには

myhw -t

のように「はてダラ」のオプションがそのまま使えることになります。

また、 設定ファイルの利用も検討してみてください。

よくある質問(FAQ)

FAQ:ActivePerlがない

質問

Windowsで「はてダラ」を使いたいのですが、Perlをインストールしていません。 どうしたらいいですか。

回答

ActiveState社のActivePerlなら、 ダウンロードしてすぐにインストールできます。 無料です。

ActivePerlから入手してください。

FAQ:LWP::UserAgentでエラーになる

質問

「はてダラ」(hw.pl)を動かそうとしたら、 Can't locate LWP/UserAgent.pm in @INC ... というエラーになってしまいます。 どうしたらいいですか。

回答

このエラーメッセージはLWP::UserAgentモジュールが見つからないときに表示されます。

WindowsのActivePerl 5.8.2以降の場合、 LWP::UserAgentモジュールは自動的にインストールされますので、 このメッセージがでることはないはずです。CPANから自力でダウンロードしようとせず、 ActivePerlを正しくインストールしたほうがよいと思います。

Linuxの場合、 LWP::UserAgentモジュールがインストールされていない可能性が考えられます。 LWP::UserAgentモジュールをインストールするには、 まずrootになり、インターネットにつないだ状態で以下のように入力します。

# perl -MCPAN -e shell
cpan> install LWP

この記述は、 Tamio Tsukamotoさんの日記を参考にさせていただきました。

FAQ:「はてダラ」でログインできない

質問

「はてダラ」(hw.pl)を動かすと、 ERROR: Login: Unexpected response: 200 OK と表示されて終了します。 どうしたらいいですか。

回答

ユーザ名またはパスワードを確かめてください。

「はてダラ」(hw.pl)のバージョン 0.3.1以前にはActivePerl 5.8.2で動かないという不具合がありました。 「はてダラ」(hw.pl)の最新版で試してみてください。

FAQ:ログインできない(SSL)

質問

Windowsで「はてダラ」を使おうとしたら、 ERROR: Login: Unexpected response: 501 Protocol scheme 'https' is not supported (Crypt::SSLeay not installed) とエラーになりました。 どうしたらいいですか。

回答

「はてダラ」1.1.0からはSSLが必須になりました。 以下のページを参照してCrypt::SSLeayをインストールしてください。

また、 「この環境で動きました」情報には、実際に試したはてなユーザへのリンクなどがあります。

FAQ:その日のタイトルをつけたくない

質問

ファイルの1行目がタイトルになるようですが、 タイトルをつけたくありません。 どうしたらいいですか。

回答

ファイルの1行目を空行にしてください。

FAQ:「はてダラ」で、はてなダイアリーの◎◎◎という機能は使えないのか

質問

「はてダラ」(hw.pl)で、はてなダイアリーの◎◎◎という機能を使いたいです (◎◎◎=設定、スタイルの変更、コメント、トラックバック…)。 どうしたらいいですか。

回答

「はてダラ」(hw.pl)は日記の更新をするだけのスクリプトですので、 このドキュメントに書かれている以上のことはできません。 通常通りWebブラウザから行ってください。

設定やスタイルの変更については、別ツール はてダコが使えるかもしれません。

FAQ:「はてダラ」でログアウトしたくない

質問

「はてダラ」(hw.pl)を動かすと、 ブラウザがはてなダイアリーからログアウトしてしまうようです。 コメントをつけるときなどに不便なのでログアウトしないようにしたいです。 どうしたらいいですか。

回答

-c というオプションをつけてみてください。 -u ユーザ名 と併用することをお勧めします。

-c というオプションをつけると、ログイン情報をcookie.txtというファイルに保存し、 そのファイルがある限り、ログイン・ログアウトを省略するようになります。

-c をつけてエラーになった場合にはcookie.txtが自動的に削除されます。 エラー後、ユーザが再実行するとユーザ名を問い合わせてきます。 それをわずらわしく感じる方は、-cオプションと-uオプションを併用するとよいでしょう。

FAQ:画像ファイルを送信したい

質問

日記と一緒に画像ファイルも送信したいです。 どうしたらいいですか。

回答

日記ファイルと同じ場所に YYYY-MM-DD.jpg, YYYY-MM-DD.png, YYYY-MM-DD.gif のいずれかにマッチする画像ファイルがあると、 自動的に送信します。 検索はこの順序です、同じ日付で先に見つかったものを送信します。

画像ファイルのタイムスタンプは見ていませんので、画像ファイルだけを更新しても送信はされません。 日記は更新していないけれど画像ファイルだけを再送信したいなら、 対応する日記ファイルを-fオプションを使って強制的に送信すればよいでしょう。

「はてダラ」経由で画像の削除はできません。

日記ファイル2004-08-24.txtと同じディレクトリに、2004-08-24.jpgという画像ファイルを置いておきます。 すると、2004-08-24.txtを送信するタイミングで、2004-08-24.jpgが一緒に送信され、その日の日記の画像になります。

FAQ:書きかけの日記を表示したくない

質問

YYYY-MM-DD.txtというファイル形式で日記を書いている途中の状態で、 誤って「はてダラ」(hw.pl)を動かすと、書きかけの日記が表示されてしまいます。 どうしたらいいですか。

回答

方法1: 書きかけの間はYYYY-MM-DD.txtではなく別の名前にしておく(たとえば draft.txt のように)。そうすれば送信されません。

方法2: ファイルの一行目をdeleteにしておく。ただし、誤って送信した場合にはその日の日記は消えますのでご注意。

方法3: はてなダイアリーの下書き機能(下記)を使う。

><!--

この範囲は下書きになります。

--><

FAQ:はてなグループの日記更新に使いたい

質問

「はてダラ」をはてなグループの日記更新に使いたいです。 どうしたらいいですか。

回答

次のような手順を踏むとよいでしょう。

たとえば、こんな風になります。

Windows (Shift_JIS)用

# はてなグループの日記を更新するための設定ファイルconfig.txt (Windows用)
id:yourid
password:xxxxxxxx
g:yourgroup
client_encoding:Shift_JIS
server_encoding:UTF-8

Linux (EUC-JP)用

# はてなグループの日記を更新するための設定ファイルconfig.txt (Windows用)
id:yourid
password:xxxxxxxx
g:yourgroup
client_encoding:EUC-JP
server_encoding:UTF-8

注意:上記の方法でうまくいくようならば、これ以降の説明を読む必要はありません。

Encodingモジュールが使えない場合には、次のような手順を踏むとよいでしょう。

フィルタコマンド

「はてなグループ」の日記は文字コードがUTF-8になっているので、 フィルタコマンドを使って、UTF-8に変換してやる必要があります。 フィルタコマンドは「%sで指定したファイルの内容を読み取ってUTF-8に変換し、標準出力に出力する」という機能を持つ必要があります。 あなたの動かしている環境で上記のようなコマンドを構成してください。

例: Jcodeモジュールを使う

たとえば、PerlでJcodeモジュールがインストールされているならば、 以下のようなスクリプトをutf8.plというファイルで保存します。

use strict;
use Jcode;
my $filename = $ARGV[0];
unless ($filename) {
    die "Usage: perl utf8.pl YYYY-MM-DD.txt\n";
}
open(FILE, $filename) or die "$!: $filename\n";
my $file = join('', <FILE>);
close(FILE);
print Jcode->new($file)->h2z->utf8;

そして、config.txtを次のように指定します。

id:yourid
# password:xxxxxx
# cookie:cookie.txt
filter:perl utf8.pl %s
g:groupid

メモ:Jcodeモジュールのインストール

Jcode.pmのWebページからJcode-X.XX.zipをダウンロードして解凍し、INSTALLファイルを読んでください。 Windowsの場合には、コマンドラインから perl win_install.plを実行します。

(この部分は、 id:yms-zunさんからの情報を元にメモしました)

例: nkfコマンドを使う

Windows版nkfコマンドは、Windowsで使える文字コード変換ツールです。

Linux版のnkfもあります。 nkfのページ

UTF-8に変換する場合のconfig.txtは、以下のようになります。

id:yourid
# password:xxxxxx
# cookie:cookie.txt
filter:nkf -w %s
g:groupid

(nkfコマンドは、 id:ishinaoさんからご教示いただきました)

FAQ:プロキシー経由ではてなダイアリーを更新したい

質問

「はてダラ」をHTTPプロキシー経由で使いたいです。 どうしたらいいですか。

回答

設定ファイルconfig.txtproxyコマンドを使えばできます。 たとえばHTTPプロキシーのURLがhttp://www.example.com:8080/の場合、 設定ファイルconfig.txtに以下の行を追加します。

proxy:http://www.example.com:8080/

FAQ:日記ファイルを1つにまとめたい

質問

「はてダラ」(hw.pl)は便利ですが、日記ファイルが日付ごとにばらばらになるのは嫌です。 日記ファイルを1つのファイルにまとめたいのですが、 どうしたらいいですか。

回答

「はてダラ」(hw.pl)をサポートするツールの1つに「はてダラスプリッタ」(hws.pl)があります。 はてダラスプリッタを使うと、diary.txtという1つのファイルをはてダラ形式の複数ファイルに分解します。 分解後、はてダラを使って送信してください。

FAQ:他のツールを作りたい(ライセンス)

質問

「はてダラ」(hw.pl)を利用して、別のツールを作りたくなってきました。 どうしたらいいですか。ライセンスはどうなっていますか。

回答

大歓迎です。 「はてダラ」はフリーソフトウェアですので、 自由に修正、再配布することができます。

「はてダラ」のライセンスはPerlと同じ(GPL or Artistic)です。

以下も参考に。

FAQ:-tでキーワードリンクが消える

質問

はてダラ1.0.2で-tオプションを使うとキーワードリンクが消えてしまう現象に遭遇しました。-tオプションを使うと、 スコアが「50」より大きいキーワードを自動リンクになるように設定されてしまいました。 ただ、必ずこの現象が起きるかは検証しきれていません。 当方が利用しているのはバージョン1.0.2、OSはWinXP Pro、Perlは5.8.4です。

回答

すみません。現状は回避策は見つかっていません。

http://d.hatena.ne.jp/rna/20040923#p4

FAQ:バグ・改善・意見・希望を送りたい

質問

このツールのバグ・改善・意見・希望を作者に伝えたいです。 どうしたらいいですか。

回答

ありがとうございます。 フィードバックのフォームからお願いします。 お返事はお約束できませんが、フィードバックは大歓迎です。 「こういう環境で動きました」という報告もありがたいです。

「この環境で動きました」情報

「はてダラ」の「この環境で動きました」情報をぜひお寄せください。

OSの種類とバージョン, Perlの種類とバージョンなどを、 フィードバックからお送りください。

このほかにも、 はてなダイアリーのキーワード 「はてなダイアリーライター」から、 はてなダイアリーではてダラに言及している人のページを見つけることができます。

関連ツール: はてダラスプリッタ(hws.pl)

「はてダラスプリッタ」(hws.pl)は1個のファイルdiary.txtをはてダラ形式の複数ファイルに分解するツールです。 毎日の日記ファイルのMD5値をファイルmd5.txtに保存しますので、必要な日記ファイルだけが更新されます。 これを使うと、日記を書くときに編集するファイルがいつもdiary.txtだけになって便利です。

はてダラスプリッタの使用例

perl hws.pl             ←ここでdiary.txtをはてダラ形式の日記ファイルに分解
perl hw.pl              ←ここで送信

はてダラスプリッタ(hws.pl)のスクリプト

#!/usr/bin/perl
#
# hws.pl - Splitter for Hatena Diary Writer.
#
# Copyright (C) 2004 by Hiroshi Yuki.
# <hyuki@hyuki.com>
# http://www.hyuki.com/techinfo/hatena_diary_writer.html
#
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
use strict;
use Digest::MD5 qw(md5_hex);

my $VERSION = "0.1.0";

my %diary;
my $diary_file = "diary.txt";
my $backup_file = "backup_diary.txt";
my $md5_file = "md5.txt";

# Scan diary.
{
    # Read.
    open(FILE, $diary_file) or die "$!: $diary_file\n";
    my $file = join('', <FILE>);
    close(FILE);

    # Replace.
    my $newfile = $file;
    $newfile =~ s/^\*t\*/"*" . time() . "*"/gem;

    # Write if replaced.
    if ($newfile ne $file) {
        # Backup.
        open(FILE, "> $backup_file") or die "$!: $backup_file\n";
        print FILE $file;
        close(FILE);
        # Save.
        open(FILE, "> $diary_file") or die "$!: $diary_file\n";
        print FILE $newfile;
        close(FILE);
    }

    # Process it.
    my ($date, $title) = ('', '');
    for (split(/\n/, $newfile)) {
        if (/^(\d\d\d\d-\d\d-\d\d):(.*)/) {
            ($date, $title) = ($1, $2);
            if ($diary{$date}) {
                die "ERROR: Duplicate $date. ($title)\n";
            }
            $diary{$date}->{content} = "$title\n";
        } elsif ($date) {
            $diary{$date}->{content} .= "$_\n";
        } else {
            print "Before dateline: ", "$_\n";
        }
    }
}

# Compute MD5.
foreach (keys %diary) {
    $diary{$_}->{md5} = md5_hex($diary{$_}->{content});
    $diary{$_}->{update} = 1;
}

# Compare diary.
if (open(FILE, $md5_file)) {
    while (<FILE>) {
        chomp;
        my ($date, $md5) = split(/:/, $_);
        if ($diary{$date}->{md5} eq $md5) {
            $diary{$date}->{update} = 0;
        }
    }
    close(FILE);
}

# Save diary files.
for (sort keys %diary) {
    if ($diary{$_}->{update}) {
        print "Create $_.txt\n";
        open(FILE, "> $_.txt") or die "$!: $_.txt\n";
        print FILE $diary{$_}->{content};
        close(FILE);
    }
}

# Update md5 file.
open(FILE, "> $md5_file") or die "$!: $md5_file\n";
for (sort keys %diary) {
    print FILE $_, ":", $diary{$_}->{md5}, "\n";
}
close(FILE);

はてダラスプリッタのdiary.txt例

はてダラスプリッタのdiary.txtの例を示します。 perl hws.plを動かすと、以下のdiary.txtを元にして2004-08-23.txtと2004-08-24.txtという2つのファイルを作ります。

注意!hws.plを動かす前にカレントディレクトリのバックアップを取ってから試してください。

2004-08-23:タイトルA
*おはよう
ここに8/23の朝の日記。
ここに8/23の朝の日記。
*お昼だ
ここに8/23の昼の日記。
ここに8/23の昼の日記。
2004-08-24:タイトルB
*おはよう
ここに8/24の朝の日記。
ここに8/24の朝の日記。
*お昼だ
ここに8/24の昼の日記。
ここに8/24の昼の日記。

以下のようにタイトルをdeleteにすれば、8/24の日記を削除できます。

2004-08-23:タイトルA
*おはよう
ここに8/23の朝の日記。
ここに8/23の朝の日記。
*お昼だ
ここに8/23の昼の日記。
ここに8/23の昼の日記。
2004-08-24:delete
*おはよう
ここに8/24の朝の日記。
ここに8/24の朝の日記。
*お昼だ
ここに8/24の昼の日記。
ここに8/24の昼の日記。

更新履歴

関連リンク

はてダラ関連ツールリンク

そのほかリンク

「はてダラ」という略称は、 なんばりょうすけさんの日記からいただきました。ありがとうございます。

作成メモ

ぜひ、感想をお送りください

あなたのご意見・感想をお送りください。 あなたの一言が大きなはげみとなりますので、どんなことでもどうぞ。

あなたの名前: メール:
学年・職業など: 年齢: 男性女性
(上の情報は、いずれも未記入でかまいません)

お手数ですが、以下の問いに答えてから送信してください(迷惑書き込み防止のため)。
今年は西暦何年ですか?

何かの理由でうまく送れない場合にはメールhyuki dot mail at hyuki dot comあてにお願いします。

豊かな人生のための四つの法則