ショートカット・ディレクトリとプレインテキストの変換
機械学習関連情報の収集と分類(構想)の❺の詳細です。
あまり応用の効かない情報ですので Qiita 的には面白くないかもしれませんが、メモとして残しておきます。
(1) 概要
ネットサーフィンして発見した記事を手動で分類するには様々なやり方が考えられます。
もっとも素直なのはブラウザやクラウドのブックマークに登録することでしょう。しかし、実際にやってみるとプルダウンメニューの操作が必要だったりして意外に時間がかかる。
色々試行錯誤した結果、エクスプローラーのツリービューを用いてドラッグ&ドロップでショートカットを配置するのが、(操作への慣れもあって)単位時間当たり一番効率が良いという結論になりました。
しかしファイルシステム上に分類結果を置いたまま利用するのはかなり不便です。
・OSに依存するファイル名
ブラウザから取得してドラッグ&ドロップでつくるショートカットのファイル名はもとのウェブページの< Title/>部分をファイル名にします。OSによってファイル名に使える文字の種類が異なり、またファイル名に許容される長さも異なります。
・大量の細切れファイル
何千個もの細切れショートカットファイルができるので、その読み取りアクセスが無駄に頻繁になります。
そこで、機械学習関連情報の収集と分類(構想)の❺の処理によって、ブックマークをプレインテキストにまとめてしまい、そのプレインテキストを以降の処理のインプットにしようというわけです。
(2) ショートカットファイル
ショートカット・ディレクトリをプレインテキスト化にする際には、基本的には各ショートカットはそのまま内容をデッドコピーすればいいのですが、ひとつだけ注意しなければならないのがタイムスタンプです。
Modified という項目があってショートカットの更新日時を表現しているようです。
InternetShortcutについてなどを参考に、Modified 文字列を日時に変換するコードを書いてみたのが下記の< serial.rb >です。
require 'when_exe'
require 'when_exe/core/extension'
include When
Epoch = when?('1600-12-30T08:59:59.844+09:00:00')
def serial2date(serial)
seed = 0
8.times do |i|
seed = seed * 256 + serial[(-4-2*i)..(-3-2*i)].to_i(16)
end
Epoch + When::PT1S * (seed / 10_000_000.0)
end
Modified は特定の元期からの経過時間を100ナノ秒単位で計算した結果を16進で表現したもののようです。when_exe gem を避けたければ適当に修正してください。
(3) ショートカット・ディレクトリ→プレインテキスト
ショートカット・ディレクトリをプレインテキスト化するスクリプト< dir2list.rb >を下記に示します。
ショートカットファイル名の先頭に'★'をつけて重要であることを示すようにしているのはローカルルールです。
ショートカットファイルの読み込みでエラーが起こるのはOSに依存するようなファイル名が使われているケースですので、そのような場合あらかじめ手動でファイル名を変更しておく必要があります。
=begin
Usage:
ruby dir2list.rb <root> (<filter>) > <list>
root : インターネットショートカットを階層的に配置したファイル群のルートディレクトリ名
filter : リスト化するディレクトリのリスト(省略すると全ディレクトリをリスト化)
list : インターネットショートカットのリスト(プレインテキスト)
=end
require './serial'
Encoding.default_external = 'UTF-8'
Encoding.default_internal = 'UTF-8'
root, filter = ARGV
root += '/'
if filter
ex = []
IO.foreach(filter) do |line|
ex << line.chomp.gsub("/", "\\/")
end
filter = /^(#{ex.join('|')})/
end
Dir.glob(root + '**/*.*') do |path|
next unless path =~ /^(.+)\.(url|website)$/i
title = $1.gsub(/%7f/i, '%7E').sub(root, '')
next if filter && filter !~ title
timestamp =
begin
File.stat(path).mtime.to_tm_pos
rescue => e
STDERR.puts e
next
end
contents = []
IO.foreach(path) do |line|
begin
case line
when /^URL/, /^IconFile/
contents << line
when /^Modified=([0-9A-F]+)/i
serial = $1
contents << "Modified=#{serial}"
timestamp = serial2date(serial)
end
rescue ArgumentError
end
end
raise ArgementError, "#{path} is empty" if contents.empty?
important = title.sub!(/\/★/, '/')
puts '%s%s %s' % [important ? '+' : '=', timestamp.to_s, title]
puts contents
puts
end
プレインテキストの中身は下記のようなものの羅列になります。
=2016-10-21T09:56:55.00+09:00 Computer/トピック/機械学習/応用/自然言語処理/Ruby からの Watson Natural Language Classifier 利用例 - Qiita
URL=http://qiita.com/suchowan/items/4bb4db40c4ff434ee847
最新のプレインテキストについては機械学習関連ブックマークなどをご覧ください。
(4) プレインテキスト→ショートカット・ディレクトリ
プレインテキストをショートカット・ディレクトリ化するスクリプト< list2dir.rb >を下記に示します。
=begin
Usage:
ruby list2dir.rb <list> <root>
list : インターネットショートカットのリスト(プレインテキスト)
root : インターネットショートカットを階層的に配置するファイル群のルートディレクトリ名
=end
require 'fileutils'
require './serial'
Encoding.default_external = 'UTF-8'
Encoding.default_internal = 'UTF-8'
def url(root, title, importance, timestamp, contents)
title.sub!(/([^\/]+)$/, '★\1') if importance == '+'
timestamp = timestamp.to_time
path = root + title
dir = path.split('/')[0..-2].join('/')
FileUtils.mkdir_p(dir) unless FileTest.exist?(dir)
file = path +'.url'
open(file, 'w') do |url|
url.puts('[InternetShortcut]')
contents.each do |content|
url.puts content
end
end
File::utime(timestamp, timestamp, file)
end
list, root = ARGV
root += '/'
title, importance, timestamp, contents = nil
IO.foreach(list) do |line|
case line
when /^([-+=])(.+?)\s+(.+)\s*/
importance, date, title = $~[1..3]
timestamp = When.when?(date)
contents = []
when /^Modified=(.+)$/
contents << line.chomp
timestamp = serial2date($1)
when /^\s*$/
url(root, title, importance, timestamp, contents) unless contents.empty?
contents.clear
else
contents << line.chomp
end
end
(5) プレインテキスト→クロールド・コンテンツ・ディレクトリ
ショートカット・ディレクトリを参照して、その実体をクロールして取得するスクリプト< crawl.rb >を下記に示します。
簡単のため URL の拡張子部分が pdf かどうかだけで取得方法を切り替えていますが、実際にはもっと細かく判断する必要があるでしょうね。
=begin
Usage:
ruby crawl.rb <root> (<filter>)
root : インターネットショートカットを階層的に配置したファイル群のルートディレクトリ名(ショートカット・ディレクトリ)
filter : クロールするディレクトリのリスト(省略すると“computer.filter.txt”を使用)
=end
require 'open-uri'
require 'openssl'
require 'fileutils'
require './serial'
Encoding.default_external = 'UTF-8'
Encoding.default_internal = 'UTF-8'
def crawl(path, timestamp, url)
timestamp = timestamp.to_time
path.sub!('/', '.crawled/')
path.sub!(/\.(url|website)$/i, url =~ /\.pdf(#.+)?$/i ? '.pdf' : '.html')
dir = path.split('/')[0..-2].join('/')
FileUtils.mkdir_p(dir) unless FileTest.exist?(dir)
unless File.exist?(path)
begin
puts path
args = [url.sub(/#[^#]*$/,''), path =~ /\.pdf$/ ? 'rb' : 'r:utf-8']
args << {:ssl_verify_mode=>OpenSSL::SSL::VERIFY_NONE} if url =~ /\Ahttps:/
open(*args) do |source|
open(path, path =~ /\.pdf$/ ? 'wb' : 'w:utf-8') do |crawled|
crawled.write(source.read)
end
end
File::utime(timestamp, timestamp, path)
rescue
File.delete(path) if File.exist?(path)
end
end
end
root, filter = ARGV
root += '/'
ex = []
IO.foreach(filter || 'computer.filter.txt') do |line|
ex << line.chomp.gsub("/", "\\/")
end
filter = /^(#{ex.join('|')})/
Dir.glob(root + '**/*.*') do |path|
next unless path =~ /^(.+)\.(url|website)$/i
title = $1.gsub(/%7f/i, '%7E').sub(root, '')
next if filter && filter !~ title
timestamp =
begin
File.stat(path).mtime.to_tm_pos
rescue => e
STDERR.puts e
next
end
contents = []
url = nil
IO.foreach(path) do |line|
begin
case line
when /^URL=(.+)/, /^IconFile/
contents << line
url = $1 if $1
when /^Modified=([0-9A-F]+)/i
serial = $1
contents << "Modified=#{serial}"
timestamp = serial2date(serial)
end
rescue ArgumentError
end
end
raise ArgementError, "#{path} is empty" if contents.empty?
crawl(path, timestamp, url)
end