読者です 読者をやめる 読者になる 読者になる

kurukuru-papaのブログ

主に、ソフトウェア開発に関連したメモを書き溜めたいと思います。

Javaで日時を扱う(Java8)

Java

Java8では、日時を扱うクラス群が様変わりしてしまいました。これを機に、日時の扱いをまとめてみたいと思います。前回は、Java7までの日時、今回は、Java8での日時を書きました。

Java8では、Java7までのDate,Calendarの代替として、次のクラスなどが導入されましたね。

現在/指定日時の作成

Instantクラスの現在/指定日時を作成する。

// 現在日時
Instant nowInstant = Instant.now();
// long→Instant
Instant instant2 = Instant.ofEpochMilli(msec1);
// LocalDateTime→Instant
Instant instant3b = localDt1.toInstant(ZoneId.systemDefault().getRules().getOffset(Instant.EPOCH));
// ZonedDateTime→Instant
Instant instant4ca = zonedDt1.toInstant();
Instant instant4cb = Instant.from(zonedDt1);
// 文字列→Instant
Instant instant5 = Instant.parse("2007-12-03T10:15:30.00Z");

LocalDateTimeクラスの現在/指定日時を作成する。

// 現在日時
LocalDateTime nowLocalDt = LocalDateTime.now();
// 指定日時
LocalDateTime localDt2 = LocalDateTime.of(2016, 5, 2, 15, 0, 0);
// long→LocalDateTime
LocalDateTime localDt3 = LocalDateTime.ofInstant(Instant.ofEpochMilli(msec1), ZoneId.systemDefault());
// Instant→LocalDateTime
LocalDateTime localDt4 = LocalDateTime.ofInstant(instant1, ZoneId.systemDefault());
// ZonedDateTime→LocalDateTime
LocalDateTime localDt5a = zonedDt1.toLocalDateTime();
LocalDateTime localDt5b = LocalDateTime.from(zonedDt1);
// 文字列→LocalDateTime
LocalDateTime localDt6a = LocalDateTime.parse("2007-12-03T10:15:30.123");
LocalDateTime localDt6b = LocalDateTime.parse("2007/12/03 10:15:30.123",
		DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"));

ZonedDateTimeの現在/指定日時を作成する。

// 現在日時
ZonedDateTime nowZonedDt = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// 指定日時
ZonedDateTime zonedDt2 = ZonedDateTime.of(2016, 5, 2, 15, 0, 0, 0, ZoneId.of("Asia/Tokyo"));
// long→ZonedDateTime
ZonedDateTime zonedDt3 = ZonedDateTime.ofInstant(Instant.ofEpochMilli(msec1), ZoneId.systemDefault());
// Instant→ZonedDateTime
ZonedDateTime zonedDt4a = instant1.atZone(ZoneId.systemDefault());
ZonedDateTime zonedDt4b = ZonedDateTime.ofInstant(instant1, ZoneId.systemDefault());
// LocalDateTime→ZonedDateTime
ZonedDateTime zonedDt5a = localDt1.atZone(ZoneId.systemDefault());
ZonedDateTime zonedDt5b = ZonedDateTime.ofLocal(localDt1, ZoneId.systemDefault(), null);
// 文字列→ZonedDateTime型
ZonedDateTime zonedDt6a = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
ZonedDateTime zonedDt6b = ZonedDateTime.parse("2016/05/02 10:15:30 Asia/Tokyo",
		DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss VV"));
ZonedDateTime zonedDt6c = ZonedDateTime.parse("2016/05/02 10:15:30 JST",
		DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss zzz"));

文字列の現在/指定日時を作成する。

// Instant→文字列
String instantStr1 = DateTimeFormatter.ISO_INSTANT.format(instant1);
String instantStr2 = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")
		.format(LocalDateTime.ofInstant(instant1, ZoneId.systemDefault()));
// LocalDateTime→文字列
String localStr1 = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS").format(localDt1);
// ZonedDateTime→文字列
// "xxxxx VV" - (例)"+09:00 Asia/Tokyo"
// "xxxx zzz" - (例)"+0900 JST"
String zonedStr1a = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSSxxxxx VV").format(zonedDt1);
String zonedStr1b = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSSxxxx zzz").format(zonedDt1);

日時から日付/時刻の取り出し

// Instant 日時→日付
Instant ymdInstant1 = instant1.truncatedTo(ChronoUnit.DAYS);

// LocalDateTime 日時→日付、時刻
LocalDate localDate1 = localDt1.toLocalDate();
LocalTime localTime1 = localDt1.toLocalTime();

// ZonedDateTime 日時→日付、時刻
ZonedDateTime ymdZoned1 = zonedDt1.truncatedTo(ChronoUnit.DAYS);
LocalDate localDate2 = zonedDt1.toLocalDate();
LocalTime localTime2 = zonedDt1.toLocalTime();

曜日の取得

// Instant→曜日
String instantWeek1 = DateTimeFormatter.ofPattern("E, EEEE", Locale.JAPANESE)
		.format(LocalDateTime.ofInstant(instant1, ZoneId.systemDefault()));
String instantWeek2 = LocalDateTime.ofInstant(instant1, ZoneId.systemDefault()).getDayOfWeek()
		.getDisplayName(TextStyle.FULL, Locale.JAPANESE);

// LocalDateTime→曜日
// ※"E","EEEE"は、タイムゾーンによって曜日の表記が変わる。
String localWeek1 = DateTimeFormatter.ofPattern("E, EEEE", Locale.JAPANESE).format(localDt1);
String localWeek2 = localDt1.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPANESE);

// ZonedDateTime→曜日
// ※"E","EEEE"は、タイムゾーンによって曜日の表記が変わる。
String zonedWeek1 = DateTimeFormatter.ofPattern("E, EEEE", Locale.JAPANESE).format(zonedDt1);
String zonedWeek2 = zonedDt1.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPANESE);

日時計

加算/減算

// LocalDateTime 1秒後、1日後、1ヶ月後
LocalDateTime localNextSec = localDt1.plusSeconds(1);
LocalDateTime localNextDay = localDt1.plusDays(1);
LocalDateTime localNextMonth = localDt1.plusMonths(1);
// ZonedDateTime 1秒後、1日後、1ヶ月後
ZonedDateTime zonedNextSec = zonedDt1.plusSeconds(1);
ZonedDateTime zonedNextDay = zonedDt1.plusDays(1);
ZonedDateTime zonedNextMonth = zonedDt1.plusMonths(1);

月初/月末を求める。

// LocalDateTime 月初、月末
LocalDateTime localMonthBeginning = localDt1.with(TemporalAdjusters.firstDayOfMonth());
LocalDateTime localMonthEnd = localDt1.with(TemporalAdjusters.lastDayOfMonth());
// ZonedDateTime 月初、月末
ZonedDateTime zonedMonthBeginning = zonedDt1.with(TemporalAdjusters.firstDayOfMonth());
ZonedDateTime zonedMonthEnd = zonedDt1.with(TemporalAdjusters.lastDayOfMonth());

差を求める。

// LocalDateTime 日時の差 ミリ秒単位、日数単位(切り捨て)
long localDiffMsec = ChronoUnit.MILLIS.between(localDt1, localNextDay);
long localDiffDays1 = ChronoUnit.DAYS.between(localDt1, localNextDay);
long localDiffDays2 = ChronoUnit.DAYS.between(localDt1.toLocalDate(), localNextDay.toLocalDate());
// LocalDateTime 日時の差 時分秒単位、年月日単位
Duration localDuration = Duration.between(localDt1, localNextDay);
Period localPeriod = Period.between(localDt1.toLocalDate(), localNextDay.toLocalDate());
// ZonedDateTime 日時の差 ミリ秒単位、日数単位(切り捨て)
long zonedDiffMsec = ChronoUnit.MILLIS.between(zonedDt1, zonedNextDay);
long zonedDiffDays1 = ChronoUnit.DAYS.between(zonedDt1, zonedNextDay);
long zonedDiffDays2 = ChronoUnit.DAYS.between(zonedDt1.toLocalDate(), zonedNextDay.toLocalDate());
// ZonedDateTime 日時の差 時分秒単位、年月日単位
Duration zonedDuration = Duration.between(zonedDt1, zonedNextDay);
Period zonedPeriod = Period.between(zonedDt1.toLocalDate(), zonedNextDay.toLocalDate());

比較する。

// LocalDateTime
boolean localAfterFlag = localNextDay.isAfter(localDt1);
// ZonedDateTime
boolean zonedAfterFlag = zonedNextDay.isAfter(zonedDt1);

参考

Javaで日時を扱う(Java7まで)

Java

Java8では、日時を扱うクラス群が様変わりしてしまいました。これを機に、日時の扱いをまとめてみたいと思います。今回は、Java7までの日時、次回は、Java8での日時を書きたいと思います。

Java7までは、主に、Dateクラス、Calendarクラスを使いますね。Apache Commons LangのDateUtils、DateFormatUtilsも使えると便利。

Apache Commons Lang について

現在/指定日時の作成

Dateクラスの現在/指定日時を作成する。

// 現在日時
Date nowDate1 = new Date();
// 文字列→Date
DateFormat dateTimeFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date1 = dateTimeFormat.parse("2016/5/2 15:00:00");
// 文字列→Date(Apache Commons Lang)
// ※Java5から、書式引数を可変長引数として記述できるようになった。
Date date1b = DateUtils.parseDate("2016/5/2 15:00:00", "yyyy/MM/dd HH:mm:ss");
Date date1c = DateUtils.parseDate("2016-5-2 15:00:00", "yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss");
// Calendar→Date
Date date2 = new GregorianCalendar(2016, 5 - 1, 2, 15, 0, 0).getTime();
// long→Date
Date date3 = new Date(System.currentTimeMillis());

Calendarクラスの現在/指定日時を作成する。

// 現在日時
Calendar nowCal1 = Calendar.getInstance();
// 指定日時
Calendar cal1 = new GregorianCalendar(2016, 5 - 1, 2, 15, 0, 0);
// Date→Calendar
Calendar cal2 = Calendar.getInstance();
cal2.setTime(date1);
// Date→Calendar(Apache Commons Lang)
Calendar cal2b = DateUtils.toCalendar(date1);
// long→Calendar
Calendar cal3 = Calendar.getInstance();
cal3.setTimeInMillis(System.currentTimeMillis());

long型(ミリ秒)の現在/指定日時を作成する。

// 現在日時
long nowMsec1 = System.currentTimeMillis();
// 指定日時
long msec1 = System.currentTimeMillis() - 24 * 60 * 60 * 1000;
// Date→long
long msec2 = date1.getTime();
// Calendar→long
long msec3 = cal1.getTimeInMillis();

文字列の現在/指定日時を作成する。

// Date→文字列
DateFormat timestampFormat1 = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
String str1 = timestampFormat1.format(date1);
// Date→文字列(Apache Commons Lang)
String str1b = DateFormatUtils.format(date1, "yyyy/MM/dd HH:mm:ss.SSS");
// Calendar→文字列
String str2 = format.format(cal1.getTime());
// long→文字列
String str3 = format.format(new Date(msec1));

日時から日付の取り出し

// Date 日時→日付(時刻ゼロ)
Calendar tmpCal1 = Calendar.getInstance();
tmpCal1.setTime(date1);
Calendar tmpCal2 = new GregorianCalendar(tmpCal1.get(Calendar.YEAR), tmpCal1.get(Calendar.MONTH),
		tmpCal1.get(Calendar.DATE));
Date ymdDate1 = tmpCal2.getTime();

// Calendar 日時→日付(時刻ゼロ)
Calendar ymdCal1 = new GregorianCalendar(cal1.get(Calendar.YEAR), cal1.get(Calendar.MONTH),
		cal1.get(Calendar.DATE));

// Date 日時→日付(時刻ゼロ)(Apache Commons Lang)
Date ymdDate2 = DateUtils.truncate(date1, Calendar.DATE);
// Calendar 日時→日付(時刻ゼロ)(Apache Commons Lang)
Calendar ymdCal2 = DateUtils.truncate(cal1, Calendar.DATE);

// 上記以外に、書式"yyyy/MM/dd"のフォーマットで文字列として取得することも可能。

日時から時刻の取り出し

// 時刻のみを取り出すことは出来ない模様。
// 必要なら、書式"HH:mm:ss"のフォーマットで文字列として取得する(?)

曜日の取得

// Dateから曜日取得
// ※ロケールによって曜日の表記が変わる。
DateFormat weekFormat1 = new SimpleDateFormat("E", Locale.JAPANESE);
DateFormat weekFormat2 = new SimpleDateFormat("EEEE", Locale.JAPANESE);
String weekStr1 = weekFormat1.format(date1);
String weekStr2 = weekFormat2.format(date1);

// Calendarから曜日取得
int weekInt1 = cal1.get(Calendar.DAY_OF_WEEK);
String[] weekNames = new String[] { "日", "月", "火", "水", "木", "金", "土" };
String weekStr3 = weekNames[weekInt1 - 1];

// Java6では、Calendar.getDisplayNameメソッドが使える。
String weekStr4 = cal1.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.JAPANESE);
String weekStr5 = cal1.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.JAPANESE);

// Dateから曜日取得(Apache Commons Lang)
// ※ロケールによって曜日の表記が変わる。
String weekStr6 = DateFormatUtils.format(date1, "E", Locale.JAPANESE);
String weekStr7 = DateFormatUtils.format(date1, "EEEE", Locale.JAPANESE);
// Calendarから曜日取得(Apache Commons Lang)
// ※ロケールによって曜日の表記が変わる。
String weekStr8 = DateFormatUtils.format(cal1, "E", Locale.JAPANESE);
String weekStr9 = DateFormatUtils.format(cal1, "EEEE", Locale.JAPANESE);

日時計

加算/減算

// Date 1秒後、1日後
// ※Java5では、TimeUnitが使えるようになった。
Date nextSecDate = new Date(msec1 + TimeUnit.SECONDS.toMillis(1));
Date nextDayDate = new Date(msec1 + TimeUnit.DAYS.toMillis(1));

// Calendar 1秒後、1日後、1ヶ月後
Calendar nextSecCal = (Calendar) cal1.clone();
nextSecCal.add(Calendar.SECOND, 1);
Calendar nextDayCal = (Calendar) cal1.clone();
nextDayCal.add(Calendar.DATE, 1);
Calendar nextMonthCal = (Calendar) cal1.clone();
nextMonthCal.add(Calendar.MONTH, 1);

// Date 1秒後、1日後(Apache Commons Lang)
Date nextSecDate2 = DateUtils.addSeconds(date1, 1);
Date nextDayDate2 = DateUtils.addDays(date1, 1);
Date nextMonthDate2 = DateUtils.addMonths(date1, 1);

月初/月末を求める。

// Calendar 月初、月末
Calendar monthBeginning = (Calendar) cal1.clone();
monthBeginning.set(Calendar.DATE, 1);
Calendar monthEnd = (Calendar) cal1.clone();
monthEnd.set(Calendar.DATE, cal1.getActualMaximum(Calendar.DATE));

差を求める。

// Date 日時の差 ミリ秒単位、日数単位(切り捨て)
// ※日付の差がほしいときは、日時→日付のみに変換して計算する。
long dateDiffMsec = nextDayDate.getTime() - date1.getTime();
long dateDiffDays = (nextDayDate.getTime() - date1.getTime()) / TimeUnit.DAYS.toMillis(1);

// Calendar 日時の差 ミリ秒単位、日数単位(切り捨て)
// ※日付の差がほしいときは、日時→日付のみに変換して計算する。
long calDiffMsec = nextDayCal.getTimeInMillis() - cal1.getTimeInMillis();
long calDiffDays = (nextDayCal.getTimeInMillis() - cal1.getTimeInMillis()) / TimeUnit.DAYS.toMillis(1);

// Date 日付の差(Apache Commons Lang)
long dateDiffDays2 = (DateUtils.truncate(nextDayDate, Calendar.DATE).getTime()
	- DateUtils.truncate(date1, Calendar.DATE).getTime()) / TimeUnit.DAYS.toMillis(1);
// Calendar 日付の差(Apache Commons Lang)
long calDiffDays2 = (DateUtils.truncate(nextDayCal, Calendar.DATE).getTimeInMillis()
	- DateUtils.truncate(cal1, Calendar.DATE).getTimeInMillis()) / TimeUnit.DAYS.toMillis(1);

比較する。

// Dateの前後比較
boolean dateBeforeFlag = date1.before(nextDayDate);
boolean dateAfterFlag = nextDayDate.after(date1);
// Calendarの前後比較
boolean calBeforeFlag = cal1.before(nextDayCal);
boolean calAfterFlag = nextDayCal.after(cal1);

// Date 日付一致確認(Apache Commons Lang)
// ※時刻は無視して、日付のみ比較される。
boolean dateSameFlag = DateUtils.isSameDay(date1, nextSecDate);
// Calendar 日付一致確認(Apache Commons Lang)
// ※時刻は無視して、日付のみ比較される。
boolean calSameFlag = DateUtils.isSameDay(cal1, nextSecCal);

おまけ:スリープ

Java5のTimeUnitを使うと、好きな時間単位でスリープ時間を書きやすい。

TimeUnit.SECONDS.sleep(1);

参考

ラズパイでJava8を使う

Java

自分のRaspberry Piに、Java8をインストールしたので、メモを残しておきます。特に、難しいことはありませんでした。

事前確認

現在のJavaインストール状態を確認します。Java7がインストールされていました。

$ java -version
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) Client VM (build 24.0-b56, mixed mode)
$
$ which java
/usr/bin/java
$ ls -l /usr/bin/java
lrwxrwxrwx 1 root root 22 Jan  8  2014 /usr/bin/java -> /etc/alternatives/java
$ ls -l /etc/alternatives/java
lrwxrwxrwx 1 root root 44 Jan  8  2014 /etc/alternatives/java -> /usr/lib/jvm/jdk-7-oracle-armhf/jre/bin/java
$ ls -l /usr/lib/jvm/jdk-7-oracle-armhf/jre/bin/java
-rwxr-xr-x 1 root root 5924 Aug 27  2013 /usr/lib/jvm/jdk-7-oracle-armhf/jre/bin/java
$
$ dpkg -l | grep jdk
ii  oracle-java7-jdk                      1.7.0+update40                          armhf        Java? Platform, Standard Edition 7 Development Kit

aptでインストール可能なJavaを確認します。Java8がインストールできることを確認できました。

$ apt-cache search oracle jdk
oracle-java7-jdk - Java? Platform, Standard Edition 7 Development Kit
oracle-java8-jdk - Java? Platform, Standard Edition 8 Development Kit

OSアップデート

以降の作業を安定して進めるため、次のコマンドでOSをアップデートしました。

$ sudo apt-get update

Java8インストール

次のコマンドでインストールしました。

$ sudo apt-get install oracle-java8-jdk
$ dpkg -l | grep oracle
ii  oracle-java7-jdk                      1.7.0+update40                          armhf        Java? Platform, Standard Edition 7 Development Kit
ii  oracle-java8-jdk                      8                                       armhf        Java? Platform, Standard Edition 8 Development Kit
$ java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) Client VM (build 25.0-b70, mixed mode)
$ which java
/usr/bin/java
$ ls -l /usr/bin/java
lrwxrwxrwx 1 root root 22  1月  8  2014 /usr/bin/java -> /etc/alternatives/java
$ ls -l /etc/alternatives/java
lrwxrwxrwx 1 root root 51  5月 14 21:47 /etc/alternatives/java -> /usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt/jre/bin/java
$ ls -l /usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt/jre/bin/java
-rwxr-xr-x 1 root root 5912  3月  5  2014 /usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt/jre/bin/java

設定

環境変数JAVA_HOMEを使用するアプリケーションのため、次の設定をしておきます。

$ JAVA_HOME=/usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt

もし、今回インストールする前のJava7を使いたい場合は、次の設定で使えるようになります。

$ JAVA_HOME=/usr/lib/jvm/jdk-7-oracle-armhf
$ PATH=${JAVA_HOME}/bin:${PATH}
$ java -version
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) Client VM (build 24.0-b56, mixed mode)

ラズパイでRubyのWebクローリング/スクレイピング環境構築メモ(後半)

Ruby

前回の続きです。

前回は、OSアップデートから、Rubyのインストールまでの手順を書きました。今回は、Webクローリング/スクレイピングするためのRubyジェムをインストールする手順です。

Rubyジェムインストール(Selenium-WebDriver,Nokogiri)

RubyでWebクローリング/スクレイピングするのに便利なジェムとして、Selenium-WebDriver、Nokogiriをインストールすることにしました。インストールは、次のように行いました。

$ gem install selenium-webdriver
$ gem install nokogiri

Selenium-WebDriverが動作するように次の設定を行いました。この設定が必須なのかどうかは不明です。

$ sudo vi /etc/hosts
$ diff /etc/hosts.bk20160403 /etc/hosts
2,6c2,6
< ::1           localhost ip6-localhost ip6-loopback
< fe00::0               ip6-localnet
< ff00::0               ip6-mcastprefix
< ff02::1               ip6-allnodes
< ff02::2               ip6-allrouters
---
> #::1          localhost ip6-localhost ip6-loopback
> #fe00::0              ip6-localnet
> #ff00::0              ip6-mcastprefix
> #ff02::1              ip6-allnodes
> #ff02::2              ip6-allrouters

Selenium-WebDriverから制御可能なブラウザとして、Firefoxを使うことにしました。Linuxの世界では、Firefoxの代替(?)が、Iceweaselらしいので次のようにしてインストールしました。

$ sudo apt-get install iceweasel

ここまでの手順で、ラズパイのX端末環境において、RubyでWebクローリング/スクレイピングができるようになりました。ただし、X端末が必要ですし、Selenium-WebDriver動作時にはブラウザが画面表示されます。これだと、CUI環境で動作させることはできませんでした。

トラブル体験

hostsファイルのIPv6設定を有効にしたまま、RubyジェムのSelenium-WebDriverを使うと、次のようなエラーが発生しました。

/home/xxx/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/selenium-webdriver-2.53.0/lib/selenium/webdriver/common/port_prober.rb:47:in `initialize': Address family not supported by protocol - socket(2) for "::1" port 7055 (Errno::EAFNOSUPPORT)

このエラーは、次のページをみて解決することができました。感謝!

blog.livedoor.jp

Firefox(Iceweasel)をインストールせずに、RubyジェムのSelenium-WebDriverを使うと、次のようなエラーが発生しました。

/home/xxx/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/selenium-webdriver-2.53.0/lib/selenium/webdriver/firefox/binary.rb:150:in `path': Could not find Firefox binary (os=linux). Make sure Firefox is installed or set the path manually with Selenium::WebDriver::Firefox::Binary.path= (Selenium::WebDriver::Error::WebDriverError)

このエラー発生時には、次のページにお世話になりました。このぺーじでIceweaselの存在を知り、インストール方法が分かりました。

assimane.blog.so-net.ne.jp

ここまでの手順の状態(CUI環境で動作させるための環境構築を未実施)にも関わらず、CUI環境でRubyジェムのSelenium-WebDriverを使うと、次のようなエラーが発生しました。

/home/xxx/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/selenium-webdriver-2.53.0/lib/selenium/webdriver/firefox/launcher.rb:90:in `connect_until_stable': unable to obtain stable firefox connection in 60 seconds (127.0.0.1:7055) (Selenium::WebDriver::Error::WebDriverError)

このとき、次のページを見つけて、Headlessの存在を知りました。

Raspberry PiでSeleniumを使ってスクレイピング | あっかぎのページ

Rubyジェムインストール(Headless)

今回初めて知ったのですが、Selenium-WebDriverをCUI環境で動作させるためには、Headlessというジェムがありました。次のようにして、Headlessジェムと、そのジェムを動作させるためのライブラリ類をインストールしました。

$ sudo apt-get install xvfb
$ gem install headless
$ sudo vi /etc/modprobe.d/ipv6.conf
$ diff /etc/modprobe.d/ipv6.conf.bk20160403 /etc/modprobe.d/ipv6.conf
3c3,4
< #alias ipv6 off
---
> alias ipv6 off

これで、CUI環境でも、Selenium-WebDriverが動作するようになりました。

トラブル体験

Selenium-WebDriverでブラウザを上手く起動できない時がありました。その場合、`export DISPLAY=:99`としてから、動作させると上手く起動できました。でも、最終的にはその手順をしなくても動作するようになりましたので、必須の手順ではないようです。

Selenium-WebDriverでWebクローリングしたところ、取得したWebページが英語表記になってしまうことがありました。その場合は、Rubyスクリプトで次のようにすることで解決しました。

profile = Selenium::WebDriver::Firefox::Profile.new
profile['intl.accept_languages'] = "ja"
profile['general.useragent.locale'] = "ja-JP"
driver = Selenium::WebDriver.for :firefox, :profile => profile

この際は、こちらのページに助けられました。

qiita.com

Selenium-WebDriverでWebクローリングしたところ、画面表示に時間がかかるページがあり、次のようなエラーが発生しました。

/home/xxx/.rbenv/versions/2.2.4/lib/ruby/2.2.0/net/protocol.rb:158:in `rescue in rbuf_fill': Net::ReadTimeout (Net::ReadTimeout)

その場合、Rubyスクリプトで次のようにすると解決しました。

# タイムアウト値を大きくする(デフォルトは60秒。)
client = Selenium::WebDriver::Remote::Http::Default.new
client.timeout = 120
driver = Selenium::WebDriver.for :firefox, :http_client => client

この際は、こちらのページに助けられました。

qiita.com

おわり

非常に手間と時間がかかりましたが、環境構築を無事完了できてよかった。

ラズパイでRubyのWebクローリング/スクレイピング環境構築メモ(前半)

Ruby

先日、ラズパイ上で、Ruby,SeleniumWebDriverのWebクローラー/スクレイピング環境を構築しました。Windows上では環境構築したことがありましたが、Linux系OS上では初めて環境構築しました。手間取ったことも多く、いくつか試行錯誤しながら作業したので、備忘録として手順をまとめたいと思います。

記事が長くなるので、2部構成にします。前半は、OSアップデートから、Rubyのインストール手順です。後半は、Webクローリング/スクレイピングするためのRubyジェムをインストールする手順です。

では、前半です。

動作確認環境

環境構築前は、次のような環境でした。

今回の作業後は、次の環境になりました。変更点のみ記述します。

作業の流れ

  • OSアップデート
  • Ruby管理ツールインストール
  • Rubyインストール
  • Rubyジェムインストール(Selenium-WebDriver,Nokogiri)
  • Rubyジェムインストール(Headless)

Rubyをインストールするまでの手順は、次のサイトに従って行いました。

hyottokoaloha.hatenablog.com

OSアップデート

以降の作業を安定して進めるため、次のコマンドでOSをアップデートしました。

$ sudo apt-get update

補足

今回作業の中で、試行錯誤した際に、次のコマンドも実行しました。必要な作業であったかどうかは不明です。

$ sudo apt-get upgrade

Ruby管理ツールインストール

今回初めて、rbenvの存在を知りました。バージョン違いのRubyをインストール、管理するツールのようです。Rubyには、下位互換性が低いバージョンがいくつかあるようなので、複数バージョンを管理できるツールがあるのは便利ですね。

次のコマンドでインストールしました。

$ sudo apt-get install rbenv
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

次のコマンドで、インストールしたコマンドが使用可能な状態になったことを確認しました。

$ rbenv -h

トラブル体験

ruby-buildは、apt-getでもインストールできました。その場合、インストール可能なRubyのバージョンが「2.0.0-dev」までしか表示されませんでした。

apt-getとgitの両方のruby-buildをインストールしてしまったことがありました。その場合、`rbenv install --list`ではインストール可能なRubyバージョンが色々表示されましたが、`ruby-build --definitions`では「2.0.0-dev」までしか表示されず、`rbenv install 2.2.4`を実行すると、なぜかコマンドの使用方法が表示され、インストールできませんでした。

apt-getでruby-buildをインストールしたときは、次のコマンドでアンインストールしました。

$ sudo apt-get remove ruby-build
$ sudo apt-get autoremove

Rubyインストール

まずは、次のようにして、Rubyで必要となるライブラリ類をインストールし、その設定を行いました。

$ sudo apt-get install autoconf
$ sudo apt-get install libreadline-dev
$ sudo apt-get install libssl-dev
$ sudo apt-get install ruby-dev
$ dpkg -l autoconf libreadline-dev libssl-dev ruby-dev
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ . .bashrc

次に、インストール可能なRubyバージョンを確認し、インストールしました。インストールしたRubyバージョンは、普段使っているWindows環境に合わせ2.2.4にしました。インストールには数時間かかり、動作しているかどうか不安になるため、オプション「-v」を付けて、詳細メッセージを表示するようにしました。

$ rbenv install --list
$ rbenv install -v 2.2.4
$ rbenv versions
2.2.4 (set by /home/xxx/.rbenv/version)

次のようにして、バージョン2.2.4が適用されていることを確認しました。

$ rbenv global 2.2.4
$ rbenv rehash
$ ruby -v
ruby 2.2.4p230 (2015-12-16 revision 53155) [armv6l-linux-eabihf]
$ gem -v
2.4.5.1

トラブル体験

autoconfのインストールをせずに作業を進めたときは、`rbenv install`のときに、次のようなエラーが発生してしまいました。

/usr/bin/ruby-build: 行 161: autoconf: コマンドが見つかりません

libreadline-devのインストールをせずに作業を進めたときは、`rbenv install`のときに、次のようなエラーが発生してしまいました。

Try running `apt-get install -y libreadline-dev` to fetch missing dependencies.

後半へ

前半は、ここまでです。後半は、こちらです。

初めてのNode.js学習メモ

Node.js

先日、Webクローラー機械学習という言葉に惹かれて、「JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック」という書籍を購入しました。
 Node.jsやWebクローラー開発に触れたことがありませんでしたが、書籍を読みつつ、手を動かしてみることで、簡単なWebクローラー作成ができるようになったと思います。ここでは、個人的な学習のまとめを書いてみたいと思います。
 なお、機械学習については、まだ理解が追いついていませんのでここでは記述しません。

前提

  • Java,JavaScript,HTMLを使ったWebアプリケーションの開発技術については学習経験があります。
  • Node.jsは未経験。
  • Windows環境を使用。
  • Node.js バージョン4.2.6 を使用。

Node.jsとは

JavaScriptエンジンの一つで、コマンドラインから利用できる。最も普及しているJavaScriptエンジンは、各種ブラウザであるが、セキュリティ面の考慮から、いろいろ制限が多い。Node.jsでは、ファイル処理やネットワーク処理などもできる。さらに、パッケージマネージャー「npm」があり、便利な機能を拡張することが容易。

Node.jsインストール方法

公式サイトから、MSIファイルをダウンロードして、インストール。
※EXEファイル形式もあったけど、npmが入っていなかった。

Node.js

インストールが終わると、スタートメニューに「Node.js command prompt」が登録されている。これを起動して、次のコマンドを実行できればOK。Windows標準のコマンドプロンプトからも実行できた。

node -v

パッケージマネージャー npm

npmは、Node.jsで、モジュール(拡張機能)をインストールしたりするためのパッケージマネージャー。インストールしたモジュールの管理場所は、グローバルとローカルの2種類がある。
次のようにコマンドラインから使用できる。

npm -v

主な使い方。

npm -h ヘルプを表示。
npm install モジュール名 モジュールをインストールする。※1
npm uninstall モジュール名 モジュールをアンインストールする。※1
npm list インストールされているモジュールの一覧を表示する。※1
npm root -g グローバル環境のパスを表示する。ホームディレクトリ\AppData\Roaming\npm\node_modules となるようです。

※1 上記コマンドでは、ローカル環境を対象とする。ローカル環境とはカレントディレクトリのこと。グローバル環境を対象とする場合は、「-g」オプションをつける。

私が試した時には、グローバル環境にインストールしたモジュールを使おうとして、「Cannot find module XXX」とエラー表示されることがありました。その場合、次のように環境変数を設定すると、解決しました。

set NODE_PATH=グローバル環境のパス(npm root -gの結果)

簡単なサンプル

ハローワールドを出力させる。

ワンライナーで実行。

node -e "console.log('Hello World');"

スクリプトを作成して実行する。

helloworld.jsファイル
console.log("Hello World");
node helloworld.js

Node.jsのリファレンス
Node.js v4.3.1 Manual & Documentation


Webページダウンロード(cheerio-httpcliモジュール)

静的なページや、RSS, XMLダウンロードなどに使用可能。逆に、jQuery,Ajaxを多用したようなページでは使用困難な模様。jQuery風に要素を指定できるのが便利。

モジュールインストール

npm install -g cheerio-httpcli

私がインストールしたバージョン
cheerio-httpcli 0.6.4

簡単なスクリプト
Googleで「node.js」を検索して、ページタイトルを標準出力する。

cheerio-httpcli-01.jsファイル
var client = require('cheerio-httpcli');

client.fetch('http://www.google.com/search',
  { q: 'node.js' },
  function (err, $, res) {
    console.log($('title').text());
});

実行

node cheerio-httpcli-01.js

参考ページ
cheerio-httpcli
Node.jsのスクレイピングモジュール「cheerio-httpcli」が第3形態に進化したようです - Qiita
Node.jsのスクレイピングモジュール「cheerio-httpcli」が大規模アップデートして帰ってきた - Qiita


Webページダウンロード(PhantomJS,CasperJSモジュール)

PhantomJSが画面表示しないWebブラウザのようなもの、CasperJSがPhantomJSを簡単に使うためのもの、らしい。
cheerio-httpcliでは操作できなかったWebページでも操作できた。コマンドラインから、casperjsコマンドでJavaScriptを実行することになるが、JavaScript内で使うことができるモジュールには制限がある模様。
Pythonのインストールが必要だった。

モジュールインストール

npm install -g phantomjs
npm install -g casperjs

私がインストールしたバージョン
PhantomJS 2.1.1
CasperJS 1.1.0-beta5

簡単なスクリプト
Googleで「node.js」を検索して、ページタイトルを標準出力する。

casperjs-01.jsファイル
var casper = require("casper").create();

casper.start("http://www.google.com/search?q=node.js", function(){
  this.echo("Title: " + this.getTitle());
});

casper.run();

実行

casperjs casperjs-01.js

参考
CasperJS documentation — CasperJS 1.1.0-DEV documentation


メール送信

Yahoo!JAPANメールを使って、メール送信してみる。

モジュールインストール
Yahoo!JAPANメールを使う場合、SMTPサーバを指定してメール送信する。SMTPサーバを使う場合、nodemailer-smtp-transportをインストールしないとダメだった。nodemailerのバージョンによるのかもしれない。

npm install -g nodemailer
npm install -g nodemailer-smtp-transport

私がインストールしたバージョン
nodemailer 2.1.0
nodemailer-smtp-transport 2.2.0

簡単なスクリプト

nodemailer01.jsファイル
// 実行方法
if (process.argv.length <= 2) {
  console.info("Usage: node nodemailer01 user pass");
  process.exit();
}
var user = process.argv[2];
var pass = process.argv[3];

// SMTPトランスポート作成
var smtpConfig = {
  host: "smtp.mail.yahoo.co.jp",
  port: 465,
  secure: true,
  auth: {
    user: user,
    pass: pass
  },
  // logger: true,
  // debug: true
};
var nodemailer = require("nodemailer");
var smtpTransport = require("nodemailer-smtp-transport");
var transport = nodemailer.createTransport(smtpTransport(smtpConfig));

// メールアドレス
var address = user + "@yahoo.co.jp";

// メール情報の作成
var mailOptions = {
  from: address,
  to: address,
  // from: '"Sender Name" <' + address + '>',
  // to: '"Receiver Name" <' + address + '>',
  subject: "テストメール",
  text: "テストメールです。"
};

// メール送信
transport.sendMail(mailOptions, function(error, response){
  if (error) {
    console.log(error);
    throw error;
  } else {
    console.log("Message sent: " + response.message);
  }
  transport.close();
});

参考
Node.jsでメール送信が出来る – 野村空っぽい


補助的な機能

実行環境取得

console.log("カレントディレクトリ\n" + process.cwd());
console.log("スクリプトパス(絶対パス)\n" + __filename);
console.log("スクリプトディレクトリ\n" + __dirname);
var path = require("path");
console.log("スクリプトディレクトリ\n" + path.dirname(__filename));
console.log("スクリプト名\n" + path.basename(__filename));
console.log("スクリプト名(拡張子なし)\n" + path.basename(__filename, path.extname(__filename)));
console.log("スクリプト拡張子\n" + path.extname(__filename));
console.log("実行時引数\n", process.argv);
console.log("実行時引数の数\n", process.argv.length);
console.log("Node.jsコマンド\n" + process.argv[0]);
console.log("スクリプトパス\n" + process.argv[1]);
var homedir = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
console.log("ホームディレクトリ\n" + homedir);

参考
Web Tips Plus: node.js ホームディレクトリのパスを取得


環境に合った改行コード

var os = require("os");
console.log("[" + os.EOL + "]");

メッセージ出力

指定メッセージを出力する。

console.log("Hello world.");
console.info("Info log");
console.warn("Warn log");
console.error("Error log");
console.trace("Trace log"); //スタックトレースが出力される。

タイムスタンプを付加してメッセージ出力する。

var infolog = require("util").log;
infolog("Hello world.");

出力/非出力を切り替え可能なメッセージ出力。
出力したい場合は、環境変数NODE_DEBUGに、debuglog関数の引数と同じ文字列を指定しておく。カンマで区切って複数指定することも可能。

var debuglog = require("util").debuglog("myapp");
debuglog("デバッグログです。");
set NODE_DEBUG=myapp

正規表現

簡単に正規表現が使える。あ、これはフツーのJavaScriptの機能だ。

console.log("abcde12345".match(/bcd/g)); //-> [ 'bcd' ]
console.log("abcde12345".replace(/bcd/g, "BCD")); //=> aBCDe12345

参考
文字列(String)
正規表現(RegExp)


設定ファイル(configモジュール)

モジュールインストール

npm install -g config

簡単なスクリプト

script/nodejs01.jsファイル
var config = require("config");
console.log(config.test01);

簡単な設定ファイル
JSON5など、いくつかの形式が使える。

config/default.json5ファイル
// default.json5
{
  "test01": "hello world",
}

参考
node.jsのいろいろなモジュール13 – node-configで設定ファイルを切り替えたりする | Developers.IO


ファイル入出力

var fs = require("fs");

fs.writeFileSync("./file-01_out.txt", "ハローワールド", {encoding:"UTF-8"});

var text = fs.readFileSync("./file-01_out.txt", {encoding:"UTF-8"});
console.log(text);

参考
File System Node.js v4.3.1 Manual & Documentation


文字コード

Node.jsでは、内部的にはUTF-16が使用されているらしい。Node.jsは、Shift_JISには対応していないらしいので、Shift_JISを扱うときは、文字コード変換するモジュールが必要。

モジュールインストール

npm install -g iconv-lite

私がインストールしたバージョン
iconv-lite 0.4.13

iconv-liteで対応している日本語文字コードは、UTF-8, CP932, Shift_JIS, EUC-JP。個人的にはこれだけあれば十分だと思う。

var iconv = require("iconv-lite");
var fs = require("fs");

fs.writeFileSync(
  "./iconv-lite-01_out.txt",
  iconv.encode("ハローワールド", "Shift_JIS"),
  "binary");

var text = iconv.decode(
  new Buffer(fs.readFileSync("./iconv-lite-01_out.txt", "binary"), "binary"),
  "Shift_JIS");
console.log(text);

参考
GitHub - ashtuchkin/iconv-lite: Convert character encodings in pure javascript.


外部コマンド実行

同期、非同期で実行することが可能。実行する外部コマンドが標準出力などをするときは、文字コードに注意する。

同期的に、dirコマンドを実行してみる。

var childProcess = require("child_process");
var iconv = require("iconv-lite");
var buffer = childProcess.execSync("dir",
  {
    timeout: 60 * 1000, // タイムアウト1分
  });
console.log(iconv.decode(buffer, "Shift_JIS"));

参考
Child Process Node.js v4.3.1 Manual & Documentation


暗号化・復号化

Node.js標準機能のcryptoで、各種アルゴリズムで、暗号化・復号化ができる。

var crypto = require("crypto");

if (process.argv.length <= 2) {
  console.log("Usage: node crypto03.js アルゴリズム 暗号化キー 暗号化対象文字列");
  console.log("主要なアルゴリズム");
  console.log("aes128, aes192, aes256 - https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard");
  console.log("des, des3 - https://ja.wikipedia.org/wiki/Data_Encryption_Standard");
  console.log("blowfish - https://ja.wikipedia.org/wiki/Blowfish");
  console.log("アルゴリズム一覧");
  var cipers = crypto.getCiphers();
  console.log(cipers);
  return;
}

// アルゴリズム
var algorithm = process.argv[2];
console.log("アルゴリズム: " + algorithm);

// 暗号化キー
var key = process.argv[3];
console.log("暗号化キー: " + key);

// 暗号化対象文字列
var plainText = process.argv[4];
console.log("暗号化前: " + plainText);

// 暗号化
var cipher = crypto.createCipher(algorithm, key);
var cryptedText = cipher.update(plainText, "utf8", "hex");
cryptedText += cipher.final("hex");
console.log("暗号化後: " + cryptedText);

// 復号化
var decipher = crypto.createDecipher(algorithm, key);
var decryptedText = decipher.update(cryptedText, "hex", "utf8");
decryptedText += decipher.final("utf8");
console.log("復号化後: " + decryptedText);

JSONJavaScriptオブジェクトの相互変換

var obj = {
  "item1": "value1"
};

var jsonText = JSON.stringify(obj);
console.log(jsonText);

var jsonObj = JSON.parse(jsonText);
console.log(jsonObj);

XML/RSS/HTMLの解析

cheerioを使うと、jQuery風に解析出来て楽。

モジュールインストール

npm install -g cheerio

私がインストールしたバージョン
cheerio 0.20.0

簡単なスクリプト

var html = ""
  + "<html>"
  + "<body>"
  + "<h1>タイトル</h1>"
  + "</body>"
  + "</html>"
  ;
var cheerio = require("cheerio");
$ = require("cheerio").load(html);
console.log($.html());
console.log($("h1").text());

参考
GitHub - cheeriojs/cheerio: Fast, flexible, and lean implementation of core jQuery designed specifically for the server.


CSVデータの読み書き

comma-separated-valuesというモジュールをインストールすると、CSVデータを簡単に扱えるようになる。

私がインストールしたバージョン
comma-separated-values 3.6.4

簡単なスクリプト

var data = ""
  + "h1,h2,h3\r\n"
  + "v11,v12,v13\r\n"
  + "v21,v22,v23\r\n"
  + "v31,v32,v33\r\n"
  ;

var CSV = require("comma-separated-values");

// CSVテキストを読み込み、2次元配列を作成する。
var csvArray = new CSV(data).parse();
console.log(csvArray);
// ->出力結果
// [ [ 'h1', 'h2', 'h3' ],
//   [ 'v11', 'v12', 'v13' ],
//   [ 'v21', 'v22', 'v23' ],
//   [ 'v31', 'v32', 'v33' ] ]

// CSVテキストを読み込み、オブジェクト配列を作成する。
var csvObjs = new CSV(data, {header:true}).parse();
console.log(csvObjs);
// ->出力結果
// [ { h1: 'v11', h2: 'v12', h3: 'v13' },
//   { h1: 'v21', h2: 'v22', h3: 'v23' },
//   { h1: 'v31', h2: 'v32', h3: 'v33' } ]

// オブジェクト配列を、CSVテキストに変換する。
var csvText = new CSV(csvObjs).encode();
console.log(csvText);
// ->出力結果
// "v11","v12","v13"
// "v21","v22","v23"
// "v31","v32","v33"

参考
comma-separated-values

以上で、学習したことを一通り書いてみたはず。

Androidアプリ「アプリ間共有の助け Ver.1.3」公開

Android

f:id:kurukuru-papa:20150219230436p:plain f:id:kurukuru-papa:20150219230450p:plain
2015/5/9に、自作Androidアプリ「アプリ間共有の助け」のバージョン1.3を公開しました。今回は、クリップボードの内容を他のアプリに共有する機能を追加しました。よろしければ、お使いください!

変更点概要

  • クリップボードの内容を他アプリに共有する機能を追加しました。(当機能では、端末起動時に通知を表示します。そのため「端末起動時に開始」のアクセス許可が必要となります。)
  • 細かな機能改善を行いました。

動作環境

対象OS:Android 4.0以降
動作確認済み端末:docomo Galaxy S4(SC-04E)