2018年3月28日水曜日

Hello Python

この文書作成時の環境は以下です。

  • Windows 10 professional バージョン1703
  • Chocolatey v0.10.8
  • Python v3.6.4:d48eceb
  • PyCharm Community 2017.3.3

小規模なスクリプトにPythonを使いたい

ログとかをごちゃごちゃ弄るスクリプトは今までbatやPowerShellを主に使ってきた。
PowerShellはともかくbatは二度と書きたくないので、高級言語で簡単なスクリプトを書けるようになろうってんでやり玉に挙がったのがPython。なんか最近人気みたいだし。1 何よりparseargsっていうコマンドラインパーサが標準で用意されているらしい。これがとても魅力的だった。

チュートリアルを上から順番にやるってめんどくさいじゃん

この考えはあんまり良くない考え方だと思うけど、事情により「学習のための時間」が取れないことだってあるのだ。
Pythonをこれから長期的に使っていくとした場合、チュートリアルを上から実践していくのが、最終的にコストが小さくなるであろう。
時間があるならチュートリアルを上から順番にやればいいし、今後やるつもりだがすぐに動くスクリプトを要求されたので付け焼刃でPythonスクリプトを書いていく。

とりあえずPythonを使えるようにする

chocolatey2 使えば一発よ。chocolateyはwindowsの標準であるところのPackageManagement3 よりもパッケージが充実しているのでオススメ。

PowerShell(管理者)
> cinst python3

ついでにPythonのIDEであるPyCharmも入れとこう。JetBrains製のIDE好きよ。

PowerShell(管理者)
> cinst pycharm-community

PyCharmの起動は置いておいて、Pythonのインストールが完了したらおもむろにPythonインタプリタを起動

PowerShell
> python

インタプリタでHelloって言うとHelloって返してくれる。Pythonは挨拶ができるいいやつだ。

Pythonインタプリタ
>>> 'Hello'
'Hello'

Python版Hello World!

Hello.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys

sys.stdout.write('Hello World!')

まあなんとなくわかる。

shebang行

#! /usr/bin/env python
shebang(シェバン)行と呼ばれるものらしい。
3. Windows で Python を使う — Python 3.6.4 ドキュメント
Linux環境で./Hello.pyと実行したときに/usr/bin/env pythonに解決されることを意味するようだ。
詳しくは呼んでいないがWindows環境でも書いておくと可搬性が上がるらしい。

マジックコメント

# -*- coding: utf-8 -*-
ソースコードの1行目または2行目にソースコードのエンコード形式を記載するらしい。
デフォルトはUTF-8なので冗長ではあるけども、もしかしたら今後デフォルトのエンコード形式が変更になるかもしれないのでおまじない的に書いておくのがいいのかな?
2. Python インタプリタを使う — Python 3.6.4 ドキュメント

標準モジュールの読み込みと利用

pythonの標準モジュールであるsysモジュールを読み込み、利用している。

sys モジュールは、全ての Python インタープリターにビルトインされています。
6. モジュール (module) — Python 3.6.4 ドキュメント

とのこと。
こちらを利用して標準出力にHello World!を出力している。

なんか作ってみる

(必要に迫られたので)ディレクトリ内のテキストを結合するツールを作成する。
Pythonを使わずとも、あるいはPythonでももっと簡単な方法があるだろうけど、あえて不慣れなPythonで。

できたものがこちら。

Merge.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import argparse
import os

parser = argparse.ArgumentParser()
parser.add_argument('srcDir', help='Target directory contains txt', type=str)
parser.add_argument('-d', '--deleteSrc', help='delete merged source files')
args = parser.parse_args()

if not os.path.isdir(args.srcDir) :
    sys.stderr.write('argument [srcDir] is not directory.')
    sys.exit(1)

for filename in os.listdir(args.srcDir) :
    path = os.path.join(args.srcDir, filename)
    if(os.path.isdir(path)) : break
    with open(path, 'r', encoding='utf_8_sig') as f :
        for line in f:
            sys.stdout.write(line)

特徴的なのはオフサイドルール4に従ってブロックが記述されていることだろうか。

文、変数の代入、関数呼び出し

Merge.py
parser = argparse.ArgumentParser()
parser.add_argument('srcDir', help='Target directory contains txt', type=str)
parser.add_argument('-d', '--deleteSrc', help='delete merged source files')
args = parser.parse_args()

動的型付けで特に宣言なくいきなり変数に代入が可能な模様。(型ヒントは存在するようだ5
関数呼び出しはxxx(引数, 引数)という形式。文は改行で区切るのが普通らしい。

条件分岐

Merge.py
if not os.path.isdir(args.srcDir) :
    sys.stderr.write('argument [srcDir] is not directory.')
    sys.exit(1)

if文。条件式は()でくくらないのが普通らしい。
論理演算がnotandなどの英単語なのが読みやすくておしゃれ。
ちなみに、elseifではブロックが深くなっていって具合が悪いのでelifというキーワードを用いる模様。
また、Pythonで共通してブロックの開始は:でブロックの終了はインデントが戻るところ。

繰り返し

Merge.py
for filename in os.listdir(args.srcDir) :
    path = os.path.join(args.srcDir, filename)
    if(os.path.isdir(path)) : break
    with open(path, 'r', encoding='utf_8_sig') as f :
        for line in f:
            sys.stdout.write(line)

今回はリストのイテレーション。
for x in xxx :でxxxコレクションの中身をイテレーションするみたい。
withはC#のusing構文やJavaのtry-with-resource構文に相当するキーワード。ブロック終了で資源が回収されるらしい。

やってることは全部のファイルを全部のファイルごとに一行ずつ標準出力に書くとか頭の悪いことしてるので、もうちょっと頭良く処理する例を作ればよかった。

感想

ブラックボックスで構わないちょっとしたツールなんかを作るときは便利だな~って感じ。

  • 可読性に重きを置いた文法に思える
    • 現状読みなれていないので本当に読みやすいかどうかはわからない
  • 標準ライブラリが豊富
    • ライブラリの選定が必要ないのは大きな利点

[ChromeExtension]コンテキスト間メッセージ通信

この文書作成時のChromeバージョンは以下です。

  • 63.0.3239.132 (64bit)

関連記事

成果物

やりたいこと

Twitterタイムライン上の画像で右クリックコンテキストメニューしたときに以下を取得したい。

  • 画像投稿者のユーザー名
  • ツイートID
  • 複数画像の何枚目か(twitterは1ツイートに4枚まで画像を添付できる)

右クリックしたコンテキストのソースURLぐらいしか手掛かりがないので、表示中のTwitterのDOMを解析して解決する。
もちろん、TwitterのDOM構成が変わると動かなくなる。

Content Scripts を使ってtwitterにスクリプトを埋め込む

Content Scriptsは特定のページに埋め込まれ、ページのDOMにアクセス可能だ。今回はtwitter.comの中ならどこでも埋め込むようにする。jQueryも使うので埋め込んでおく。

manifest.json
{
  ...
  "content_scripts":[
     {
          "matches" : [
              "*://twitter.com/*"
          ],
          "js":[
              "scripts/getImageInfo.js",
              "scripts/jquery-3.3.1.min.js"
          ]
      }
  ],
  ...
}

content_scriptsが配列であることからわかるように、複数のページにスクリプトを埋め込むことができる。

コンテキスト間メッセージ通信

Browser Action (Page Action)によるポップアップ、Event Page、Content Scriptが埋め込まれたページ、これらは別コンテキストである。
他のコンテキストの処理を呼び出す場合には、Chrome経由でメッセージ通信を行う必要がある。

今回はコンテキストメニュークリック時にEvent PageからContent Scriptにメッセージを送信する。
また、Content ScriptでDOM解析した結果をEventPage側で同期的に処理する。
image.png (18.4 kB)

送信側

Content Script を呼び出す場合はchrome.tabs.sendMessage()を使う。
それ以外を呼び出す場合はchome.runtime.sendMessage()を使う。
注意点は以下

  • Content Scriptにメッセージを送信する場合とそれ以外に送信する場合で、APIが異なる
  • 一度も起動していないBrowser Action (Page Action)ポップアップは存在しないので、メッセージを送信できない
background.js
...
function downloadImage(srcUrl, response){
    ...
}

function messageToGetTweetInfo(info, tab){
    console.log(info);
    chrome.tabs.sendMessage(
        tab.id,
        {name: 'twitterImageDL', srcUrl: info.srcUrl},
        function(response) {downloadImage(info.srcUrl, response);}
    );
}

chrome.contextMenus.onClicked.addListener(messageToGetTweetInfo);

...

コンテキストメニューを開いたページにメッセージを送る場合は、contextMenus.onClicked.addListener()で登録した関数の
第2引数(tab)のidプロパティをtabs.sendMessage()の第1引数に入れるとよい。
tabs.sendMessageの第2引数には、受信側で行う処理を一意に特定できる識別子を入れるとよい。
同期メッセージの結果はコールバックの引数として取得できる。

ポップアップ起動時など、tabのidが不明な場合は、chrome.tabs.queryを用いてタブを取得する。

popup.js

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    chrome.tabs.sendMessage(tabs[0].id, {name:'message_name'}, function(response){
        ...
    });
});

受信側

chrome.runtime.onMessageイベントリスナーを登録する。
結果の返送は第3引数のハンドラを利用する。

getImageInfo.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    console.log(request);
    if(request.name === 'twitterImageDL'){
        ...
        sendResponse({
            result : true,
            username : username,
            tweetId : tweetId,
            imageIndex : imageIndex
        });
    }
    return true;    //  sync
});

注意点は以下

  • trueを返却すると同期実行、trueを返却しないと非同期実行
  • 同じExtension内のメッセージイベントのみが発火する(他のアプリからのメッセージを受ける場合はonMessageExternal

DOMを読む

jQueryで泥臭く読む。twimg.comのURLをsrcに持つimgタグを検索するところからスタート。特に説明は割愛。

これでユーザー名、ツイートID、画像が何枚目かを取得できた。
chrome.downloads.downloadの引数にファイル名を指定してダウンロードできるようにすれば、一旦は完成!

2018年3月23日金曜日

[ChromeExtension]右クリックからコンテンツをダウンロード

この文書作成時のChromeバージョンは以下です。

  • 63.0.3239.132 (64bit)

関連記事

成果物

やりたいこと

twitterタイムライン上の画像のオリジナルイメージを右クリックからダウンロード可能にする。
以下のAPIを使う

  • chrome.contextMenus
  • chrome.downloads

pbs.twing.comソースのときだけメニューを表示する

contextMenus.createのプロパティを書く。
contextsをimageに絞り、画像の上だけでメニューを表示するように、
documentUrlPatternsで、twitter.comのみでメニューを表示するように、
targetUrlPatternsで、画像のソースがpbs.twimg.comの場合のみにメニューを表示するように変更。

background.js

chrome.runtime.onInstalled.addListener(function(){
    chrome.contextMenus.create({
        type: 'normal',
        id: 'downloadTwitterImage',
        title: 'download original image',
        contexts: [
            'image'
        ],
        documentUrlPatterns: [
            '*://twitter.com/*'
        ],
        targetUrlPatterns: [
            '*://pbs.twimg.com/media/*'
        ]
    });
});

URLを指定してダウンロード

downloads.downloadのAPIでソースのURLに:origを付加してDLする。

background.js
function downloadImage(info, tab){
    console.log(info);
    chrome.downloads.download({
        url: (info.srcUrl + ':orig'),
        saveAs: false
    }, function(id){downloadId = id;});
}

function renameDownloads(downloadItem, suggest){
    console.log(downloadItem);
    if(downloadItem.id !== downloadId) return;
    suggest({
        filename: downloadItem.filename.replace('_orig', '')
    });
}

chrome.downloads.onDeterminingFilename.addListener(renameDownloads);
chrome.contextMenus.onClicked.addListener(downloadImage);

saveAsプロパティをtrueにすると、ダウンロード時に「名前を付けて保存」ダイアログが展開するようになる。

ファイル名を変更する

そのままDLすると拡張子がjpg_origとなり使い勝手が悪いので、downloads.onDeterminingFilenameイベントを使用して_origを取り除く。
downloads.downloadのコールバック引数でダウンロードIDが取得できるので、DownloadItem.Idと比較することで、今回ダウンロードしたもののみ変更することができる。

permissionを更新する

downloads APIを利用するので、manifestのpermissionにdownloadsを追加する。

manifest.json
{
  ...
  "permissions" : [
    "contextMenus",
    "downloads"
  ]
  ...
}

以上でオリジナル画像のダウンロードが可能になった。
次はコンテキスト間を通信して、ユーザー名とツイートIDをDOMから取得する。

2018年3月20日火曜日

PowerShellのRedirectのエンコードがUTF16固定で嵌った

この文書の作成時点の環境は以下です。

  • PowerShell 5.1.15063.909

標記のおとり。
以下のようにリダイレクトでファイル出力すると、BOM付きUTF16リトルエンディアンでエンコードされる。

PowerShell
PS> echo はろー > hello.txt 

ざっと探した感じではリダイレクトのエンコードを指定する方法は見つからない。

代わりにOut-Fileコマンドレットが利用できる。
Out-* コマンドレットを使用してデータをリダイレクトする | Microsoft Docs

PowerShell
PS> echo はろー  | Out-File -FilePath .\hello.txt -Encoding UTF-8

BOMなしは自前でがんばる必要がある。まじかよ。
いくつか方法がありそうだが、例えばText.Encoding.UTF8.GetBytes()でUTF8のByte列を取得し、Set-Contentコマンドレットで出力する方法は以下。

PowerShell
PS> echo はろー |  %{[Text.Encoding]::UTF8.GetBytes($_)} | Set-Content -Path .\hello.txt -Encoding Byte

PowerShellがちょっと嫌いになった。エンコードの違いに気付かなかった自分が悪いんだけどね。

2018年3月19日月曜日

はじめてのChromeExtension

この文書作成時のChromeバージョンは以下です。

  • 63.0.3239.132 (64bit)

関連記事

成果物

作りたいもの

世の中には絵がうまい人が溢れている。
のちのち資料として利用するために画像を保存するわけだけど、twitterに投稿されたオリジナルの画像をローカルに保存するのは結構手間。

  1. 画像を右クリックして画像アドレス(pbs.twing.com/media/xxxxxxxxxxx)をコピー
  2. 画像アドレスの末尾に「:orig」を付加してブラウザで表示
  3. 名前を付けて画像を保存で保存する

既存のものとしてはコンテキストメニューからオリジナル画像をDLするChrome拡張がいくつか公開されている。

これらの拡張は便利なのだが、デフォルトファイル名がいまいちで利用しにくい。
そこでユーザー名とツイートIDでリネームするChromeExtensionを作成しようとした。

はじめてのChromeExtension

以下は公式ドキュメントにだいたい書いてあることです。

ChromeExtension概要

ChromeExtensionはChromeに機能を追加するソフトウェアで、JavaScriptで動作する。
ChromeExtensionは主に以下のコンテキストから構成されているようだ。

  • Browser Action (Page Action)
  • Content Script
  • Event Page

これらのコンテキストをマニフェストファイルに記載することでChromeがこれを解釈できるようなる。
他にもOptionPageなどがあるようだが、メインは上記の3種類のコンテキスト。

Browser Action (Page Action)

ChromeExtension独自のHTMLページを表示する。
インストールすると右上に表示されるアイコンをクリックするとHTMLページがポップアップする。
Browser Actionは全てのページで利用することが可能で、Page Actionは特定ページでのみ利用できる。
Page Actionが利用できないときは、アイコンはグレーアウトする。1

manifestファイルにはBrowser Action か Page Actionのどちらか、または利用しない場合はどちらも記載しない。

manifest.json
{
  "name": "Extension Name",
  ...
  "browser_action": {
    "default_icon": {
      "19": "icons/icon19.png",
    },
    "default_title": "Title",
    "default_popup": "popup.html"
  },
  ...
}

Content Script

特定のページにスクリプトとスタイルシートを埋め込む。
埋め込んだページのDOM操作が可能。
ページ内のスクリプトとは独立して動作するので、ページが持っている関数などは利用できない。

manifest.json
{
    "name": "Extension Name",
    ...
    "content_scripts":[
        {
            "matches" : ["*://twitter.com/*"],
            "js":[
              "scripts/contentscript.js",
              "scripts/jquery-3.3.1.min.js"
            ]
        }
    ],
    ...
}

Event Page

バックグラウンドでスクリプトを動作させる。
目に見えないWebページが存在してそこにスクリプトが埋め込まれるイメージ。

manifest.json
{
  "name": "Extension Name",
  ...
  "background" : {
    "scripts" : ["scripts/background.js"],
    "persistent" : false
  },
  ...
}

persistentをtrueにすると Backgroud PageとなりChromeに常駐する。(常にメモリを食うので非推奨)

右クリックコンテキストメニューから Hello World !!

右クリックからこんにちは世界!

ディレクトリ構成

プロジェクトルートディレクトリにmanifest.jsonを配置する。
残りの配置は自由。今回は以下の配置とした。
アイコンはIncscapeで適当に準備した。(慣れてないので1時間かかった)

TwitterImageDL
┣ icon
┃ ┣ icon16.png
┃ ┣ icon19.png
┃ ┣ icon48.png
┃ ┗ icon128.png
┣ scripts
┃ ┗ background.js
┗ manifest.json

マニフェストファイルの記述

公式ドキュメントを読みながら必要そうなものを追加していく。

manifest.json
{
  "manifest_version" : 2,
  "name" : "Twitter Image DL",
  "short_name" : "Twitter Image DL",
  "version" : "0.0.0",
  "description" : "Download and rename original image in twitter.com.",
  "icons" : {
    "16" : "icon/icon16.png",
    "48" : "icon/icon48.png",
    "128" : "icon/icon128.png"
  },
  "background" : {
    "scripts" : ["scripts/background.js"],
    "persistent" : false
  },
  "permissions" : [
    "contextMenus"
  ]
}

スクリプト

スクリプトを書く。公式サンプルのContext Menus Sample (with Event Page)を参考に書く。Context Menus Sampleの方はEvent Pageに対応していないので、こちらを参考にすると詰まる。2
chrome.runtime.onInstalledイベントでコンテキストメニューを作成する。
イベントページ起動のタイミングでコンテキストメニューのonClickedイベントに世界とこんにちはを登録する。

background.js
function helloWorld(){
    console.log('Hello World !!');
}

chrome.runtime.onInstalled.addListener(function(){
    chrome.contextMenus.create({
        type: 'normal',
        id: 'helloWorld',
        title: 'hello world !!',
        contexts: [
            'all'
        ]
    });
});

chrome.contextMenus.onClicked.addListener(helloWorld);

デバッグ実行

chrome://extensionsにディレクトリをまるごとドラッグアンドドロップすると、インストールが完了する。
右上のチェックボックス「デベロッパーモード」を有効にして、「バックグラウンド ページ」をクリックするとイベントページを確認することができる。
拡張機能 - Google Chrome 2018-02-19 15.03.57.png (37.8 kB)

コンテキストメニューに作成したメニューが追加されていることを確認
image.png (17.3 kB)

実行するとConsoleが世界とこんにちはすることを確認
image.png (10.1 kB)

できた!
次はpbs.twing.comから画像をダウンロードする。


  1. 利用可能なら青バッジが付く仕様だったときもあったようだ。UI的にはアイコンはモノクロでないほうがよさそうだ。 

  2. chrome.contextMenus.createの引数プロパティのonClickがEvent Pageでは利用できないようだ。ドキュメントに書いてある。