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

cloverrose's blog

Python, Emacs, CI/CD, Webアプリなど

Ubuntu ServerでX serverを動かす手順メモ

AWSのEC2のUbuntu Server (Ubuntu Server 14.04 LTS (HVM), SSD Volume Type - ami-a21529cc) でX serverを動かす手順メモ

sudo apt-get update && sudo apt-get upgrade && sudo reboot
sudo apt-get install xorg
sudo apt-get install python-qt4
sudo xinit &
export DISPLAY=:0.0

xinit動かすと error setting MTRR (base = 0xf0000000, size = 0x00100000, type = 1) Invalid argument (22) のエラー出るけどPyQt4はちゃんと動作してた。今のところ問題なさそう。

PyQt4でJavaScriptでレンダリングしてるWebページをScraping! CookieとUserAgentも扱うよ

JavaScriptレンダリングしてるWebページをScrapingしたい!!!

自分の環境

  • Virtualbox (Host OS Windows 7 64bit)
  • Guest OS Ubuntu16.04.1 LTS Desktop 64bit
  • Python 2.7.12
  • PyQt4は sudo apt-get install python-qt4 でインストール

実現方法

調べたら素晴らしいBlogが見つかった。

Ultimate guide for scraping JavaScript rendered web pages | IMPYTHONIST

BlogではPyQt4のWebKitを使ってJavaScript部分もレンダリングしたHTMLを取得する方法を紹介している。

(HTMLのParseに関する話もあるが、HTMLさえ取得できればBeautifulSoup使えばいい)

このBlogはコメント欄も有用な情報がたくさんあって、同じことをする別のライブラリやプロジェクトを示唆している人もいた。

ただやりたいことに対して複雑なライブラリを使いたくなかったので、このBlogの方法で実現してみた。

Blogの通りに動かすとちゃんと http://pycoders.com/archive/スクレイピングできた!!

タスク

だいたいやりたいことの大枠はBlogのソースコードで実現できた。

あとは細かい部分を詰めていくだけ。やりたいところはこの2つ

  • Cookieを扱いたい
  • UserAgentを扱いたい

Cookieを扱いたい

Blogのコメント欄にもあるけどQNetworkCookieJarを使えばいける

実現方法の詳細はこのSOがとても参考になった。

python - Permanent cookies with QWebKit -- where to get the QNetworkAccessManager? - Stack Overflow

手順

  1. EditThisCookie などを使ってCookie情報をJSONファイルに保存しておく
  2. QNetworkCookieを作る
  3. QNetworkCookieのListを作る
  4. QNetworkCookieJarを作る
  5. QWebPageにQNetworkCookieJarをセット .networkAccessManager().setCookieJar(cookiejar)

関連クラスの仕様

UserAgentを扱いたい

このSOが参考になった。

python - Setting useragent in QWebView - Stack Overflow

自分のUserAgentはここでわかるっぽい

手順

  1. What's My User Agent? - User Agent & Browser Tools などでCookieを調べる
  2. .userAgentForUrl に このCookieを返す関数をセットする

最終結果

# -*- coding:utf-8 -*-
import datetime
import json
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
from PyQt4.QtNetwork import *


# 偽装したいUserAgent
USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
COOKIE_FILE = "cookie.json"


def customuseragent(url):
    u"""
    どのurlでも同じUserAgentを返す
    """
    return USER_AGENT


def make_cookiejar(filename):
    u"""
    JSON形式でExportされたCookieを読み込みQNetworkCookieJarを作る
    """
    with open(filename) as f:
        data = json.load(f)

    cookies = []
    for item in data:
        cookie = QNetworkCookie(item['name'], item['value'])
        if 'domain' in item:
            cookie.setDomain(item['domain'])
        if 'expirationDate' in item:
            cookie.setExpirationDate(datetime.datetime.fromtimestamp(item['expirationDate']))
        if 'hostOnly' in item:
            cookie.setHttpOnly(item['hostOnly'])
        if 'path' in item:
            cookie.setPath(item['path'])
        if 'secure' in item:
            cookie.setSecure(item['secure'])
        cookies.append(cookie)

    cookiejar = QNetworkCookieJar()
    cookiejar.setAllCookies(cookies)
    return cookiejar


#Take this class for granted.Just use result of rendering.
class Render(QWebPage):
    def __init__(self, url):
        self.app = QApplication(sys.argv)
        QWebPage.__init__(self)
        self.loadFinished.connect(self._loadFinished)

        self.userAgentForUrl = customuseragent
        cookiejar = make_cookiejar(COOKIE_FILE)
        self.networkAccessManager().setCookieJar(cookiejar)

        self.mainFrame().load(QUrl(url))
        self.app.exec_()

    def _loadFinished(self, result):
        self.frame = self.mainFrame()
        self.app.quit()


def main(url, filename):
    r = Render(url)
    result = r.frame.toHtml().toUtf8()
    with open(filename, 'w') as f:
        f.write(result)


if __name__ == '__main__':
    URL = 'http://pycoders.com/archive/'
    FILENAME = 'result.html'
    main(URL, FILENAME)

補足

Qtの独自の型があるけど

  • QByteArrayってなってるところはPythonのstr型でOK
  • QDateTimeってなってるところはPythonのDatetime型でOK
  • frame.toHtml()の結果をtoUtf8()してあげないと日本語が文字化けで?になってしまう

感想

3年前くらいにMacPyQt動かそうとしてメッチャ苦労したけどUbuntuでは超簡単に動いた。

わりとすぐにいいBlogにたどり着くことができてよかった。

PyQtのリファレンス体裁が汚すぎる…

Raspberry Pi3 起動に苦戦

昨日Raspberry Pi3 ModelBが届いた

www.amazon.co.jp

NOBESのダウンロードを30分位待って、よっしゃ遊ぶぞって思って、電源を挿したらPWRの赤色LEDは点滅するけど、モニターには何も映らない。

ぐぐったら、Raspberry Pi3からは電源として5V, 2.5Aのものを使う必要があることを知った。

あわててAmazonで注文した。 www.amazon.co.jp

次の日、アダプターが届いたので、よっしゃ遊ぶぞって思って、電源を挿したら(以下略)

www.raspberrypi.org によると

The Raspberry Pi's bootloader, built into the GPU and non-updateable, only has support for reading from FAT filesystems (both FAT16 and FAT32), and is unable to boot from an exFAT filesystem. So if you want to use NOOBS on a card that is 64GB or larger, you need to reformat it as FAT32 first before copying the NOOBS files to it.

ということで僕のSDXC64GB exFATにフォーマットされてるから読み込めないっぽい。ひとっ走りSDカード買いに行かないと。

pywebhdfsにHAとFederationをサポートするPRがマージされた

WebHDFS Hadoop python

WebHDFSについて

WebHDFShdfsコマンドではなくREST APIにhttpでアクセスできる便利なもの。

Hoop(httpfs)とwebhdfsの違い - たごもりすメモとかが図もあってわかりやすいと思う。

背景1 (hdfsコマンドへの不満)

MapReduceなどを使って解析を行って、その結果を可視化するとかを行っていると、スクリプトの中で頻繁にHDFSにアクセスすることになった。

そしてShellスクリプトが量産されるわけだが、自分はPythonスクリプトを書きたかった。

最初はsubprocessの中でhdfsコマンドを叩いていたが、レイテンシが結構あるし、lsコマンドの結果をパースしてファイル名を取得する必要があって微妙だった。

そんな話をしたらWebHDFSがあるよと教えてもらった。

背景2 (WebHDFSとの出会い)

WebHDFSに触ってみたら、レイテンシがすごく短いし、JSONが返ってくるので扱いやすい。心が踊った。

テンションが上ってHDFSのWebUIみたいなことができるシェルインタプリタを作った。 できる処理はls, cat, cd, pwdでread/get的なものだけにしていた。

その時自分でWebHDFSのREST APIのうすいラッパーを実装した。

これがわりと必要十分な機能は揃っていたためチームでもこのapi.pyが使われるようになった。

背景3 (自作ラッパーの限界)

自作ラッパーはシェルインタプリタ用に遊びで作っていたので、実業務で利用されるとカバーしているAPIが少なかった。

特に新規ファイル作成とかは、1回NameNodeにリクエストを投げて、返ってきたDataNodeのURIに対して再度ファイル内容とともにリクエストを投げるという形式で、自分で全部実装するのはめんどくさすぎた。

また認証周りもめんどくさくて、APIに渡す引数が増えていきそうだった。

そこでOSSPythonのWebHDFSラッパを探すことにした。

ライブラリ探し

PythonからHDFSを操作する - 偏った言語信者の垂れ流しに辿り着いた。

そこでは2つのライブラリが比較されていた。2013年の比較だが参考になった。

実際、PYPIを見てもWebHDFSライブラリはリリースが2014-01-20と古く、もうメンテナンスされていない気がした。

そこでpywebhdfsを使うことにした。しかしこのライブラリではHAとFederationがサポートされていなかった。

HAとFederationについて

HA(High Availability)とはHadoopのNameNodeが単一障害点だった欠点を解消するために、ActiveとStandbyという2つ以上のNameNodeを起動しておき、ActiveなNameNodeが落ちたらStandbyだったNameNodeが自動でActiveに切り替わるという仕組みで、実際の業務でHadoopを使うなら必須な機能。

Federationは複数のNameNodeがメタ情報(ディレクトリ構造とか)を分担してメモリに保持する仕組み。貧弱なメモリのNameNodeで大規模なクラスタを管理しようとすると全てのメタ情報が載り切らない。この時/data/以下はNameNode1で、/user/以下はNameNode2でそれ以外はNameNode3でという感じで分割できる仕組み。

どちらも自分たちのプロジェクトでは使っている。

現状pywebhdfsは1つのNameNodeのホスト名を渡すので、Activeが落ちた時に、APIはずっとエラーを返すようになってしまい、それを外側で検知して切り替えたりしないといけない。

HAとFederationのサポートの実装

いろいろ考えたが、pywebhdfsのIssueにもHAのサポートが欲しいという声が上がっていたので、PRを投げることにした。

しばらくの土日はいろんなパターンでHA/Federationをサポートする仕組みを実装した。

最終的には、パスにマッチする正規表現とそれに該当するNameNodeのリスト(HAならActiveとStandby)を順序付きの辞書でAPIコンストラクタに渡すことにした。

順序付きの辞書にしたのは、それ以外のパス(.*)とは最後にマッチさせたいからだ。

実装にあたって気をつけたのは、HA/Federationを利用していないユーザーには今までと同じインターフェースを保つこと。

そしてPRを投げた。

support federation and HA by cloverrose · Pull Request #22 · pywebhdfs/pywebhdfs · GitHub

もらったレビューを反映して、今朝マージされた :)

1ヶ月待っていたので感慨深かった。

f:id:cloverrose:20150718113204p:plain

余談

HDFSのシェルインタプリタは自作のapi.pyではなく、自分のパッチが当たったpywebhdfsを使って実装し直して現在も気に入って使っています。

Pythonインタプリタを作るときにはreadlineをラップしたcmdという便利なものがあります。

TABで補完とかが簡単に実装できるし、入力読み取り→実行のループも勝手にやってくれます。初めて知ったけど、今後また何か作るときに使っていきたい。

ピープルウェア 第3版 感想

感想が、読書メーターに収まらなかったのでブログに書きます。

人月の神話の中でおすすめされていたので読みました。

ピープルウエア 第3版

Amazon.co.jp: ピープルウエア 第3版: トム・デマルコ, ティモシー・リスター, 松原友夫, 山浦恒央: 本

新しい知見が得られたり、面白かった章。特にためになったのが4章と34章(☆付けた)

1章 今日もどこかでトラブルが

ハイテクの幻影

IT企業で働いてるとついつい、全て自動化できるし、テクノロジーで解決できると思ってしまうけど、製品は置いておいて、確かに実際働いてる場所では人間臭いこと、社会学的なことがいっぱいだと再認識した。

☆4章 品位質第一・・・時間さえ許せば

自分が品質に拘るタイプなので、「プログラムの品質は開発者の自尊心と強く結びついてる」というのに非常に共感できた。チームの人の自尊心についても考えていけるようになろう。

13章 オフィス環境進化論

この本のオフィス環境の部分は、自分でどうこう出来る部分が少ないのであまり実りは無かったけど、この章で出てきた参考図書「時を超えた建築の道」が面白そうだなと思った。

あと、イヤホンで音楽聞いてると、音楽は右脳で聴いているので、作業をする左脳と競合しないからエラー発生率や開発速度はそこまで落ちないけど、右脳を使うひらめきが起きにくくなるというのが目からうろこだった。

22章 ブラックチームの伝説

読み物として面白い。プログラムのテストチームの話。

24章 続、チーム殺し:残業の予期しない副作用

残業すると、自分の人生の大事な時間が削られる。チームで長期間の残業を強制すると、育児などで残業をしない人への不満が募り、うまくいっていたチームも崩壊するという話。会社との雇用契約で結んだ時間ないしか個人を束縛してはいけないなと感じた。

26章 スパゲッティディナーの効果

読み物として面白い。本能的に優れたマネージャーの話。

27章 裃を脱ぐ

チームメンバーに無理にでも出張を組んで、会社から出させてあげる。

☆34章 変化を可能にする

ちょうど自分の今の体験と重なって、とても勉強になった。

  • 人は誰しも本質的に変化を嫌う
  • 変化に対する反応は以下のように分類され、自分が変化を起こそうとするときに、1.と3.は敵、真の味方は2.であるという話
  1. むやにみ忠実(全く質問もしない)
  2. 信じているが室もなり
    1. 懐疑論者(「証拠を見せろ」)
    2. 受身の傍観者 (「私に何か関係有ることがあるの?」)
    3. 反対者(変化への恐れ)
    4. 反対者(権力を失うことへの恐れ)
  3. 戦闘的な反対者(妨害したり破壊してやろう)
  • 変化を受け入れてもらうには論理的な意見を主張するのではなく、情緒的に、変化前の人たちに敬意を持って
  • 変化のモデルは古い状況-(他からの要素)->混乱-(アイデアの変形)->実践と統合->新しい状況という4つのフェーズがある

自分の体験というのは、CI・CDツールとしてChef(Ruby)に切り替えていくのが、Fabric(Python)の知識がある状態から、ChefとRubyという初心者に変わってしまうこととか、Chef導入に素直に賛成だった人より、懐疑論者だった自分(ChefとFabricの比較、Chefに移行して得られるメリットと学習コストのデメリットなどをまとめた)の方が、Chef以降に移ってからは、Kitchenのテストツールなどの調査など、真の味方なきがするなという部分。


自分はプログラミングも好きだけど、こういうソフトウェア開発における問題とかにも興味あるので非常に面白かった。

Pythonのjsonモジュールの便利機能

Python

PythonJSONを読み書きする機会が割りとあったんですが、改めて調べたり、公式ドキュメントをちゃんと読んだら便利な機能を知ったのでメモしておきます。

jsonファイルの辞書を順番通りにloadしたい

設定ファイルとしてJSONを使っている時、普通にloadすると辞書の順番は保たれません。 でも、プログラム内部で設定を書き足してdumpするような場合、人間が書いた部分がごちゃごちゃになるとよろしくありません。

Pythonjsonモジュールでは次のようにすると順番通りに読み込めます。

import json
import collections


decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
with open('conf.json') as conf_file:
    conf = decoder.decode(conf_file.read())

参考

最初に見つけた回答 Can I get JSON to load into an OrderedDict in Python? - Stack Overflow

公式ドキュメントでは以下のように書いてあります。

object_pairs_hook はオプションで渡す関数で、ペアの順序付きリストのデコード結果に対して呼ばれます。 object_pairs_hook の返り値は dict の代わりに使われます。この機能はキーと値のデコードされる順序に依存する独自のデコーダ (たとえば collections.OrderedDict() は挿入の順序を記憶します) を実装するのに使えます。 object_hook も定義されている場合は、 object_pairs_hook が優先して使用されます。

バージョン 2.7 で変更: object_pairs_hook のサポートを追加しました。

jsonをdumpするときにの行末空白(trailing whitespace)を無くしたい

出力するJSONが人が見る設定ファイルの場合、適切に改行やインデントを設定すると思います。 しかし、indentを設定すると行末空白が付いてしまい、設定ファイルをGitなどで管理するときに気になります。

次のようにseparatorsを指定してあげると行末空白を取り除くことができます。

with open('conf.json', 'w') as conf_file:
    json.dump(conf, conf_file, indent=4, separators=(',', ': '))

参考

最初に見つけた回答 Issue 16333: Trailing whitespace in json dump when using indent - Python tracker

公式ドキュメントでは以下のように書いてあります。

separators がタプル (item_separator, dict_separator) ならば、デフォルト区切り文字 (', ', ': ') の代わりに使われます。 (',', ':') が最もコンパクトな JSON の表現です。

サンプル

順番通りにloadして、新しい設定を追加して、行末空白なしで、元の順番通りに書き出す例

# -*- coding:utf-8 -*-
import json
import collections


decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
with open('conf.json') as conf_file:
    conf = decoder.decode(conf_file.read())

conf['New.Key'] = 'append last'

with open('conf.json', 'w') as conf_file:
    json.dump(conf, conf_file, indent=4, separators=(',', ': '))

この2つは設定ファイル扱う上で今後は無くてはならない感じになりそうです。勉強になった!

EmacsでGithub flavorなMarkdownをプレビューできるようにする

Emacs Markdown

markdown-modeを設定すると、C-c C-c pでMarkdownで書かれたファイルをHTMLに変換して、ブラウザで表示してくれるのでとっても便利です。

しかし、オリジナルのMarkdown.plを使うと表を変換できない、CSSを設定しないとすごくそっけないHTMLが表示されるという不便があります。

そこで、自分が好きな変換ツール(今回はPythonのgrip)を使うように設定しました。

手順

1. Markdown-modeをインストール

2. Gripをインストール

pip install grip

参考: GitHub Flavored Markdownを変換できるgripが便利だった - cloverrose's blog

3. PATHの通ったところに以下をmarkdownというファイル名で保存

#!/bin/sh
filename=$1
~/.virtualenvs/default/bin/grip --export ${filename} > /dev/null
cat ${filename%.*}.html

説明

  • gripにレンダリングサーバを建てるのではなく、HTMLを出力するように--exportオプションを指定
  • 実行時のExporting to hoge.htmlという標準出力が邪魔なので/dev/nullに
  • markdown-modeに変換結果を渡すためにcat

4. init.elに以下の設定ファイルを追加

;; markdown mode
;; markdownモード(gfm-mode Github flavor markdown mode)を拡張子と関連付けする
(autoload 'markdown-mode "markdown-mode"
   "Major mode for editing Markdown files" t)
(add-to-list 'auto-mode-alist '("\\.text\\'" . gfm-mode))
(add-to-list 'auto-mode-alist '("\\.markdown\\'" . gfm-mode))
(add-to-list 'auto-mode-alist '("\\.md\\'" . gfm-mode))

;; ファイル内容を標準入力で渡すのではなく、ファイル名を引数として渡すように設定
(defun markdown-custom ()
  "markdown-mode-hook"
  (setq markdown-command-needs-filename t)
  )
(add-hook 'markdown-mode-hook '(lambda() (markdown-custom)))

最後に

今までMarkdownをEmacsで書いてるけど、プレビューはできていなかったので、かなり改善しました。 今回はPythonのGripを例にしましたが、markdownというShellスクリプトでラップしてあげればどんな変換ツールでも動きます。 (markdownというコマンド名もmarkdown-commandをカスタマイズすれば好きなのにできます)

補足

本来、Gripも標準入力・標準出力をサポートしているのでmarkdown-commandを"grip --export -"に設定すれば、/usr/local/bin/markdownでラップしてあげなくてもよいはず。

しかし、日本語で書かれたREADME.mdに対して、以下のコマンドを実行すると、文字コード周りのエラー出てしまう。

cat README.md | grip --export - | less

なので、ラップしてファイル名を受け取ってHTMLを出力するのが今のところ安定している。