Shizuoka.py #5お疲れ様でした

参加された皆様お疲れ様でした。

一年半ぶりでちょっと間が空きましたが次回は年末までに島田のあたりでやろうかということになったので、またみなさんで楽しく過ごせるといいかなと思います。

今回もそれなりに新しい参加者がいたので楽しかったです。発表するともっと楽しいのでちょっとしたことでもいいので発表しましょう。

それからブログみたいなとこにメモっておくだけでも自分用の備忘録にもなるし、記憶の定着度もいいのでそういうのもいいと思います。

僕も次回はもうちょっとまともなネタを仕込んで臨みたいw

それから懇親会楽しかったです。lambdaネタは某静岡の制作会社の自作自演QAだとずっと勘違いしてたのだけど疑いが晴れてよかったですw自己紹介タイムに遅れてきたヒトの救済措置はしておかないとちょっと困るかもしれないので遅刻したヒトはLT前とかに軽く自己紹介するようにしたほうがいいのかなと。

1468152716

1468152718

1468152720

ではまた次回☆

joblibのdump,load地味に便利

いつもMemoryしか使ってないんだけど、dump, load使ってみたら便利だったので次回からはこっち使う。

例えばpickleを使って永続化する場合

import pickle
test_dict = {"one": 1, "two": 2, "three": 3}

with open("test.dump2", mode='wb') as f:
    pickle.dump(test_dict, f)

with open("test.dump2", mode='rb') as f:
    print pickle.load(f)

と書かなきゃいけないところをjoblibだと

import joblib
test_dict = {"one": 1, "two": 2, "three": 3}

joblib.dump(test_dict, "test.dump")
print joblib.load("test.dump")

と書けるわけです。素敵。

requests+pyqueryで気をつけること

requests+pyqueryで文字列の扱いに悩まされるのは主に二箇所ある。そしてその組み合わせのエラーが出るので悩ましい。

一点目はrequestsでの文字化け

requestsでcontentとtextの違いはtextはr.encodingでdecodeされたunicode文字列だということだ。なので正しくデコードされたunicode文字列を推定できればrequestsでの文字化け問題は解決する

だが常にunicode文字列をpyqueryに渡せばOKかというとそうでもない。

Unicode文字列をpyqueryに渡すと起きるエラー(Unicode strings with encoding declaration are not supported.)

常にUnicode文字列を渡すようにすると Unicode strings with encoding declaration are not supported. というエラーが散見されるようになる。これはpyqueryが内部的に使っているlxmlがヘッダーにエンコーディングが指定しているHTMLを渡すとそれにあわせてデコードしようとするらしく、既にユニコード文字列化されたHTMLもその作法にしたがってエラーになるというものだ。

というわけでこの場合はもとのstr文字列を渡さないといけないらしい。

これが結構面倒くさい。requestsでヘッダーにエンコーディングが指定されているかどうか知るオプションあるんかね?

PyQueryの挙動がちょっとわからん

企業のプレスリリースをテキストマイニングしようと思うと色々大変なわけだ。そもそもプレスリリースの媒体がpdfを想定されているために、タイトルのみ本文無しという潔いRSSが多いしそういうのは購読しても嬉しくない。

というわけでプレスリリース一覧のページからpdfのURLを抜き出してpdfminerかましてデータ抽出やらないといけないしそっちのほうが逆に構造化されていてスクレイプしやすかったりする。

で、PyQueryを使ってガリガリやっていたのだが、何故かスクレイプできないサイトがあった。BeautifulSoupではちゃんとスクレイピング出来たのでエラーの原因が気になって調べてみたのでメモっておく

普通にrequestsでHTMLを取ってきてPyQueryに渡してやると :::sh >>> from pyquery import PyQuery as pq >>> import requests >>> tgpr = "http://www.transgenic.co.jp/pressrelease/" >>> d1 = pq(requests.get(tgpr).content) >>> d1 [<{http://www.w3.org/1999/xhtml}html>] >>> d1("a") []

aタグが一件も見つからない。しかし、urlオプションに直接サイトのURLを指定するときちんとparseされている

>>> pq(url="http://www.transgenic.co.jp/pressrelease/")
[<html>]
>>> d2 = pq(url="http://www.transgenic.co.jp/pressrelease/")
>>> len(d2("a"))
71

なんだこれは?となったのでドキュメントを漁ったらxmlパーサーでパースしてみてだめだったらhtmlパーサー使うぜって書いてあったので強引にxmlパーサーでparseさせてみた。

>>> d3 = pq(url="http://www.transgenic.co.jp/pressrelease/", parser="xml")
>>> d3
[<{http://www.w3.org/1999/xhtml}html>]
>>> len(d3("a"))
0

というわけで文字列を渡すときにはなぜかhtmlのパーサーでparseされていないっぽい。これはコードを追うべきですな。

67行目のetree.XMLSyntaxErrorという例外が投げられてないのではないかということになった。

まぁ別にわざわざrequests使う必要もないのでurlオプションにURL渡せばいいんだけどね。

次回のShizuoka.pyの懇親会はやきとり王将になりました

王将使うの多分初めてかな。懇親会は予約の締め切りがあるので参加される方はお早めに

昨日は久しぶりに#A君とあって一緒に黙々コーディングをしてたんだけど、A君重量級になってた…弱虫ペダルでいうと田所感が出てた(まじで運動しないと身体にくるぞw)

また@karky7@ando_ando_andoと鈴木屋でホルモンつつきながらプログラミング言語談義でもしたいなぁと思っていたけどなかなか難しいですね。

それから#A君にはgoに奪われた静岡市圏の勢力をPyhtonで奪還するというタスクが出来たので頑張ってください。たまにはコメヤスに酒を買いにいきたいし静岡で飲みたいなぁ。

で、もくもく会では2人でTDNetのクローラー動かないなー、おかしいなーと悩んでいたんだけど、selenium2.53.2とphantomjs2.1.1の組み合わせだとswitch_to_frameメソッドがきちんと動いてないっぽいですね。chromedriverだと動いたのでダウングレードして動く組み合わせを見つけるかバグフィックスされるの待たないといけませんな。

久々にもくもくしたけど楽しかった。ここ二三年一人でコーディングすることに慣れちゃったのと、技術を追いかけなくても余裕で生きていけるようなぬるま湯に浸かりきっていたので流石にまずいなーと感じたよ。

次回のShizuoka.pyでは

  • 不適切だが違法ではない♡クローラーを作ろう
  • はじめてならアコムをやめてジョブリブでキャッシングをしよう☆

という2つの発表をします。どちらもネタに走る予定なのであまり役に立たないかもしれないけどリハビリも兼ねているので大目に見てくださいw

Shizyoka.py #5を7/9@コミュニティFで開催します

一年半ぶりにShisuoka.pyをやります。沼津のプラサヴェルデを会場にしようと思っていたんだけど、別のイベントで使ってみたら、団体登録とか予約とかちょっとめんどくさかったので前回と同じコミュニティFでやることにします。

演題絶賛募集中です。僕は来週#A君(@ando_ando_ando)とSeleniumを使ってクローラー作る予定なのでその内容を発表しようかなーと考えています。

また、懇親会の場所が決まってないのでリクエスト等あれば。ビール電車もなかなか魅力的ではあるが…

pep8はもう古いのでpycodestyleを使おう

Pythonのコーディング規約をチェックするツールにpep8があるのだけど、pycodestyleっていう名前に変わったので今後はこちらをつかえということらしい。

つらつらと眺めていたらプロジェクトの統計を取るオプションが面白そうだったのでこのブログのCMSで実行してみた。

$ pycodestyle --statistics -qq .
1       E128 continuation line under-indented for visual indent
1       E203 whitespace before ':'
5       E231 missing whitespace after ','
2       E265 block comment should start with '# '
5       E271 multiple spaces after keyword
1       E302 expected 2 blank lines, found 1
11      E402 module level import not at top of file
29      E501 line too long (115 > 79 characters)
14      E711 comparison to None should be 'if cond is None:'
1       W291 trailing whitespace

一行のコードの文字数多すぎるんだよっていうエラーが一番多いけど、SQLAlchemyのクエリって長くなりがちだから しょうがないじゃん。 \で改行すると読みにくくなるから嫌いなんだよなぁ。

Emacs+flymakeでコード書いているのでpep8をpycodestyleに変更するだけでOK

TDnet? 強いよね…

TDnet? 強いよね…右クリック禁止,jsでHTML組み立て、iframeつかいまくり、button要素でクリック阻止とすきがないね。でも俺は負けないよ。どm、DOMたちが躍動する俺のseleniumを皆さんに見せたいね

というわけで 東証のサービスをスクレイピングしたい案件が発生したのだけど、あのサイトあれだった。一応個人でRSS化しているヒトはいたのだけど、今回は色々あって自分で頑張ってみた。

  • input要素ではなくbutton要素でクリックしてsubmitする感じになっていたのでsend_keys(Keys.RETURN)
  • iframeの内部には普通にはアクセス出来ないのでswitch_to_frameで移動する必要がある

コードを一部抜粋

def access(self, code):
    url = "https://www.release.tdnet.info/index.html"
    self.driver.get(url)
    iframe = self.driver.find_element_by_tag_name("iframe")
    self.driver.switch_to_frame(iframe)
    self.driver.find_element_by_name("q").send_keys("{}0".format(code))
    self.driver.find_element_by_name("q").send_keys(Keys.RETURN)
    time.sleep(2)
    results = []
    try:                                                                                                                                                          
        result_frame = self.driver.find_element_by_name("mainlist")
        self.driver.switch_to_frame(result_frame)
        main_table = self.driver.find_element_by_id("maintable")
        for tr in main_table.find_elements_by_tag_name("tr"):
            company_name = tr.find_element_by_class_name("companyname").text
            published = tr.find_element_by_class_name("time").text
            t = tr.find_element_by_class_name("title")
            title = t.find_element_by_tag_name("a").text
            url = t.find_element_by_tag_name("a").get_attribute("href")
            results.append({"company_name": company_name,
                            "published": published,
                            "title": title,
                            "url": url})
        return results
    except WebDriverException:
        logging.debug("{} not found".format(code))
        return results

ウェブのAPIとかデータベースに接続するようなプログラムを開発するときにjoblib便利すぎる

データサイエンティスト的な仕事をしたりスクレイパー的な開発をするとキャッシュしたくなることがある。

例えばどういう時かというと

  • pandas使っていてデータベースに重い重い処理を投げて帰ってきたものをd3.jsとかseabornみたいな描画処理のところで試行錯誤したい
  • requests+pyqueryとかでスクレイピング用のコード書きたいけど、相手のサイトに何度もアクセスするのは気が引ける

で、大抵そういう時ってファイルキャッシュしておくと思うんだけど、そのコードを追加するのが結構な心理的な負担になることが多いです。

本運用時に消すの忘れてアチャーってなったりするしね。

結局キャッシュ追加削除にコードの変更っていうのは面倒くさいというかよくないので、joblibを使うとハッピー

/tmpにでもキャッシュディレクトリ設定しておいてキャッシュしたい関数にデコレーターかます。これだけ

import requests
from joblib import Memory

memory = Memory(cachedir="/tmp")

@memory.cache
def get_ct():
    r = requests.get("https://clinicaltrials.gov/search?term=IPI-145&resultsxml=true")
    return r.content

if __name__ == '__main__':
    print get_ct()

本運用ではデコレーター外すだけだから心理的負担がほぼゼロ☆