エンジニアは、何も書かないのが正解

書き出し

お世話になっております。
ご返信ありがとうございます。
突然のメールで失礼いたします。御社のウェブサイトを拝見し、はじめてメールを差し上げます。
~様からの紹介でメールを差し上げます。
日頃から~をご利用いただき、誠にありがとうございます。
平素は格別のお引き立てをいただき、ありがとうございます。
ご無沙汰しております。
その後、いかがおすごしでしょうか。
立て続けのご連絡で失礼いたします。

書き終わり

どうぞ、よろしくお願い申し上げます。
何卒、よろしくお願い申し上げます。
引き続き、よろしくお願い申し上げます。
今後とも、お力添えのほど、よろしくお願い申し上げます。
勝手なお願いで大変恐縮ですが、よろしくお願い申し上げます。
ご確認のほどよろしくお願いいたします。
お手数ですが、ご返事いただければ幸いでございます。
ご検討のほど、よろしくお願い申し上げます。
お近くにお越しの際はぜひお立ち寄りください。
お会いできることを楽しみにしています。
ご参加をお待ちしております。
取り急ぎ、よろしくお願いいたします。
取り急ぎ、お礼申し上げます。
ご不明な際は、お気軽におっしゃってください。
今から楽しみでございます。
お礼を兼ねて失礼いたします。

president.jp

なろう作品をKindleで読めるePub形式に変換

つかいかた

1. この記事の下部にあるRubyスクリプトを、任意のディレクトリに"narou2epub.rb"というファイル名で保存する。

2. 以下のようにコマンドラインで実行すると、epubファイルができる。[ncode]には、なろう作品のものを指定する。

$ ruby narou2epub.rb [ncode]

注記:未インストールのときは、事前に一度だけNokogiriとRubyzipのインストールが必要。

$ gem install nokogiri
$ gem install rubyzip

3. できたファイルをメールに添付し、Kindle端末のメールアドレスに送信する。

Rubyスクリプト

require 'open-uri'
require 'nokogiri'
require 'zip'

def scrape(url)
  opt = {}
  opt['Cookie'] = "over18=no"
  charset = nil
  
  html = open(url, opt) do |io|
    charset = io.charset
    io.read
  end
  
  return Nokogiri::HTML.parse(html, nil, charset)
end

def replace(text)
  text.gsub!(/<p id="L?\d+">/, '<p>')
  text.gsub!('<br>', '<br />')
  text.gsub!(/<img .*?>/, '(画像削除)')
  text.gsub!(/<a .*?>/, '')
  text.gsub!("</a>", '')
  return text
end

def puts_novel(io, doc)
  results = doc.xpath('//div[@id="novel_p"]')
  unless results.length == 0
    io.puts replace(results.inner_html)
    io.puts '<hr />' 
  end
  
  results = doc.xpath('//div[@id="novel_honbun"]')
  io.puts replace(results.inner_html)
  
  results = doc.xpath('//div[@id="novel_a"]')
  unless results.length == 0
    io.puts '<hr />' 
    io.puts replace(results.inner_html)
  end
end

ncode = (ARGV[0].nil?) ? 'n6316bn' : ARGV[0]
doc = scrape('https://ncode.syosetu.com/'+ncode+'/')

title = doc.xpath('//p[@class="novel_title"]').inner_text
creator = doc.xpath('//div[@class="novel_writername"]').inner_text.gsub(/[\r\n]/,"").gsub(/作者:/,"")
chapters= []
episodes = []

results = doc.xpath('//dl[@class="novel_sublist2"]')
results.each do |r|
  chapter_title = r.xpath('preceding-sibling::div[@class="chapter_title"][1]').inner_text
  subtitle = r.xpath('descendant::a').inner_text
  link = r.xpath('descendant::a/@href').inner_text
  
  chapters.push chapter_title unless chapters.last == chapter_title
  episodes.push [chapters.length-1, subtitle, link]
end

io = Zip::OutputStream.open(File.basename(title)+'.epub')

io.put_next_entry('mimetype')
io.puts 'application/epub+zip'

io.put_next_entry('META-INF/container.xml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="content.opf" media-type="application/oebps-package+xml" />
  </rootfiles>
</container>
EOS

io.put_next_entry('content.opf')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0" xmlns="http://www.idpf.org/2007/opf" xml:lang="ja">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:title>#{title}</dc:title>
    <dc:creator>#{creator}</dc:creator>
    <dc:language>ja</dc:language>
  </metadata>
  <manifest>
    <item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />
    <item id="title" href="title.xhtml" media-type="application/xhtml+xml" />
    <item id="main" href="main.xhtml" media-type="application/xhtml+xml" />
    <item id="style" href="style.css" media-type="text/css" />
  </manifest>
  <spine page-progression-direction="rtl">
    <itemref idref="title" />
    <itemref idref="toc" />
    <itemref idref="main" />
  </spine>
</package>
EOS

io.put_next_entry('toc.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>目次</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
  <nav epub:type="toc">
EOS

if chapters.length > 1
  io.puts '    <h2>目次</h2>'
  io.puts '    <ol>'
  chapters.each_index {|i| io.puts '      <li><a href="main.xhtml#Ch_'+i.to_s+'">'+chapters[i]+'</a></li>'}
  io.puts '    </ol>'
elsif episodes.length > 1
  io.puts '    <h2>目次</h2>'
  io.puts '    <ol>'
  episodes.each {|e| io.puts '      <li><a href="main.xhtml#Ep'+e[2].gsub("/", "_")+'">'+e[1]+'</a></li>'}
  io.puts '    </ol>'
else
  io.puts '    <ol hidden="hidden">'
  io.puts '      <li><a href="main.xhtml">本文</a></li>'
  io.puts '    </ol>'
end

io.puts <<"EOS"
  </nav>
</body>
</html>
EOS

io.put_next_entry('style.css')
io.puts <<"EOS"
html {
-webkit-writing-mode: vertical-rl;
-epub-writing-mode: vertical-rl;
writing-mode: vertical-rl;
}
nav ol { list-style: none; }
EOS

io.put_next_entry('title.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>表題</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<h1>#{title}</h1>
#{creator}
</body>
</html>
EOS

io.put_next_entry('main.xhtml')
io.puts <<"EOS"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <title>本文</title>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
EOS

if episodes.length == 0
  io.puts '<section>'
  puts_novel(io, doc)
  io.puts '</section>'
else
  temp= nil
  episodes.each do |e|
    STDERR.puts e.join(' ')
    
    io.puts '<section>'
    unless temp == e[0]
      io.puts '<h2 id="Ch_'+e[0].to_s+'">'+chapters[e[0]]+'</h2>'
      temp = e[0]
    end
    
    io.puts '<h3 id="Ep'+e[2].gsub('/', '_')+'">'+e[1]+'</h3>'
    puts_novel(io, scrape('https://ncode.syosetu.com'+e[2]))
    io.puts '</section>'
    
    sleep 3
  end
end

io.puts '</body>'
io.puts '</html>'

io.close

今後の課題

  • 画像は表示できない。

メモ

  • 目次はulタグではなく、olタグでなければならない。代わりにCSSでlist-style: none;指定する。
  • Apple Booksはnavプロパティなしだと壊れていると判定する。一方、Kindleは表示できる。
  • 本文中にimgタグが含まれると、kindle端末が表紙と誤解してしまうため、画像を削除している。

キースイッチで、そろそろ動物園ができる。

動物園にしては、パンダが多めである。個人としては、Rhinoceros🦏が登場するのが待ちどおしい。

Linear

- Name Actuation Bottom-out
🐬 Durock Silent Linear Dolphin - 62g
🦛 Kinetic Labs Hippo - 63.5g
🐕 Kinetic Labs Husky - 63.5g
🐼 Invyr Panda - 67g
🦙 Alpaca - 62g
🦙 Silent Alpaca - 62g
🪼 Kailh Jellyfish "Y" 50±10g 60g
🐸 MOMOKA Frog 54g 62g
🦩 MOMOKA Flamingo - 67g
🐍 JWK Moyu Green Snake - 65g
🐈 Glorious Lynx - 60g
🦦 Gateron Baby Raccoon 55±8g -
🦄 Kailh Pink Unicorn 50g 63.5g
🐅 TTC Tiger 45g 55g

Tactile

- Name Actuation Bottom-out
🐼 Drop + Invyr Holy Panda - 67g
🐼 Drop Holy Panda X - 60g
🐼 Glorious Panda - 67g
🐼 FEKER Like Holy Panda 67g -
🐼 TECSEE Purple Panda 55g 67g
🐼 Outemu Panda 50g 65g
🐼 TTC Holy Panda 45±10g 55±10g
🐧 Kinetic Labs Penguin - 63.5g
🐟 Kinetic Labs Salmon - 63.5g
🦘 Gateron Baby Kangaroo - 59g
🦐 Durock Shrimp Silent T1 - 67g
🦈 MOMOKA Shark - -
🐝 GamaKay Bumblebee 45±10g 65g
🔥 GamaKay Phoenix 40±10g 45g

Clicky

- Name Actuation Bottom-out
🦊 Chosfox x Kailh Arctic Fox 52g 53g
🪼 Kailh Jellyfish "X" 47±15g -
🦉 Kailh White Owl 46g 70g

キーボードのハードウェアマクロでExcelの文字を赤色にする

背景

Excelの利点は、マクロ/関数/入力制限/条件付き書式などの機能が利用できることです。これらは技術分野との相性がよいです。一方で、Wordと違って校閲機能がないため、変更点が可視化しにくいです。

Excelで変更点を記録するには、赤字や取り消し線で記録することになります。この操作はマウスを使うのが一般的ですが、キーボードから手が離れるため、作業効率が落ちます。

ショートカットキーもあるのですが、長いため、現実的ではありません。

赤字 Alt,H,F,1,↓,↓,↓,↓,↓,↓,↓,←,←,←,←,ENTER
黒字 Alt,H,F,1,↓,←,←,←,←,ENTER


ソフトウェアでマクロを登録するなどの対策も考えられますが、会社支給パソコンへの任意ソフトウェアのインストールは、禁止されています。

対策

Keychron Q2を購入し、ハードウェアマクロを登録することにしました。VIAではなく、Remapをつかっています。

会社支給パソコンは、キーボードが日本語配列であるため、選択肢が限られます。Keychron Q2は数少ない解決策といえます。

superkopek.jpKeychron Q2 QMK カスタム・メカニカルキーボード ノブバージョン

65%日本語配列キーボード

  • キーボードは小さい方がよい。
  • 十字キーは必要である。
  • Deleteキーは必要である。(60%にはない。)
  • 会社支給パソコンは日本語配列である。

これらを叶えるのが、65%日本語配列キーボードである。

ARCHISS ProgresTouch RETRO TINY

  • 2016年販売
  • Fn+右Shift+5を押してもShift+F5にならない。

archisite.co.jp

Keychron Q2

  • 2022年発売
  • Keychronの公式サイトで販売しているISO配列より、キーが2個多い。
  • ホットスワップ
  • キーマップカスタマイズ可
  • 重く、高い。

superkopek.jpKeychron Q2 QMK カスタム・メカニカルキーボード ノブバージョン

STREAK65 JP

www.ask-corp.jp

なろう作品をSend to Kindleする(ツール不要)

背景

小説家になろう」の作品をKindleの電子インクペーパーで読みたい。以前は便利なWebサービスがあったが、つかえなくなって久しい。代替ツールはあるが、インストールに手間がかかるようだ。

Kindleは、mobiやepubのような電子書籍専用のファイル形式のほかに、textやhtmlにも対応している。よって、複数話をひとつのhtmlファイルに結合するだけで、Kindleにテキストを表示することはできる。この際、縦書きではなくてもよかろう!挿絵もいらない。

やったこと

以下のRubyスクリプトを任意のディレクトリに保存して、

$ ruby narou2zip.rb [ncode]

のようにコマンドラインで実行すると、zipファイルができる。このファイルを手動でSend to Kindleすればよい。

ただし、もし未インストールのときは、一度だけ事前にNokogiriとRubyzipのインストールが必要。

$ gem install nokogiri
$ gem install rubyzip

require 'open-uri'
require 'nokogiri'
require 'zip'


def scrape(url)
  opt = {}
  opt['Cookie'] = "over18=no"
  html = open(url, opt) do |io|
    charset = io.charset
    io.read
  end
  
  return Nokogiri::HTML.parse(html, nil, charset)
end

#
def puts_novel(io, doc)
  results = doc.xpath('//div[@id="novel_p"]')
  unless results.length == 0
    io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
    io.puts '<hr>' 
  end
  
  results = doc.xpath('//div[@id="novel_honbun"]')
  io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
  
  results = doc.xpath('//div[@id="novel_a"]')
  unless results.length == 0
    io.puts '<hr>' 
    io.puts results.inner_html.gsub(/<p id="L?\d+">/, '<p>')
  end
end

#
ncode = (ARGV[0].nil?) ? 'n6316bn' : ARGV[0]
doc = scrape('https://ncode.syosetu.com/'+ncode+'/')

title = doc.xpath('//p[@class="novel_title"]').inner_text
writer = doc.xpath('//div[@class="novel_writername"]').inner_text.gsub(/[\r\n]/,"")
chapters= []
episodes = []

results = doc.xpath('//dl[@class="novel_sublist2"]')
results.each do |r|
  chapter_title = r.xpath('preceding-sibling::div[@class="chapter_title"][1]').inner_text
  subtitle = r.xpath('descendant::a').inner_text
  link = r.xpath('descendant::a/@href').inner_text
  
  chapters.push chapter_title unless chapters.last == chapter_title
  episodes.push [chapters.length-1, subtitle, link]
end

#io = File.new(File.basename(title) + '.html', 'w')
io = Zip::OutputStream.open(ncode+'.zip')
io.put_next_entry(File.basename(title) + '.html') 

io.puts '<!DOCTYPE html>'
io.puts '<html>'
io.puts '<head>'
io.puts '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
io.puts '<title>'+title+'</title>'
io.puts '</head>'
io.puts '<body>'
io.puts '<h1>'+title+'</h1>'
io.puts writer

if chapters.length > 1
  io.puts '<h2>目次</h2>'
  chapters.each_index {|i| io.puts '<a href="#Ch_'+i.to_s+'">'+chapters[i]+'</a><br>'}
elsif episodes.length > 1
  io.puts '<h2>目次</h2>'
  episodes.each {|e| io.puts '<a href="#Ep'+e[2].gsub("/", "_")+'">'+e[1]+'</a><br>'}
else
  io.puts '<hr>'
end

if episodes.length == 0
  puts_novel(io, doc)
else
  temp= nil
  episodes.each do |e|
    STDERR.puts e.join(' ')
    
    unless temp == e[0]
      io.puts '<h2 id="Ch_'+e[0].to_s+'">'+chapters[e[0]]+'</h2>'
      temp = e[0]
    end
    
    io.puts '<h3 id="Ep'+e[2].gsub('/', '_')+'">'+e[1]+'</h3>'
    
    puts_novel(io, scrape('https://ncode.syosetu.com'+e[2]))
    sleep 3
  end
end

io.puts '</body>'
io.puts '</html>'
io.close

きづいたこと

  • 拡張子を".html"に変更するだけでは、html形式として識別されない。「<!DOCTYPE html>」や「<html></html>」などの最低限の構成要素を含めるべき。
    • 「<!DOCTYPE html>」がないと、markdown形式に認識されるらしい。なぜならば、単語の中に"*"(半角アスタリスク)があると、以降がすべて斜体で表示されていたため。
  • ページ内リンク(<a href="#sample">...</a>と<h2 id="sample">...</h2>のセット)がつかえるので、目次もかんたんに作れる。name属性も可。
  • ルビタグ(<ruby>...</ruby>)はつかえる。
  • アイテム名には、ファイル名が表示される。titleタグ(<title>...</title>)やdc:titleでは表示されない。
  • 著者名は、metaタグ(<meta name="Author" content="...">)やdc:creatorタグでは表示されない。
  • 改行コードは、macOS/UNIX(LF)でもOK。
  • CSSによる縦書き表示(writing-mode: vertical-rl;や-webkit-writing-mode: vertical-rl;)はできない。
  • 挿絵の画像は、表示できない。
  • MacOSでは、ファイル名を右クリックして圧縮したものをSend to Kindleすると、Amazonからエラーメールが返ってくる。zipコマンドで圧縮しないとだめ。
  • 短編の場合とそうでない場合との両方に対応する必要がある。
  • R18作品のときは、Cookieに"over18=yes"を指定する。

つぎやること

著者名が自動で入力できるようにしたい。