Python学習めも:0. とりあえず箱

ちょっとずつ Python を勉強している Rubyist の @riocampos です。

Python学習めも:1. Python環境構築」の次が「0. とりあえず箱」というのは数の順序的におかしいのですが、Rubyist として疑問に思った事や気付いた事などを雑多に入れていく箱が欲しかったのでこんな名前になりました。この内容はそのうち別記事になっていく…はずです。

.oO(これ、文句箱になっている気がする…。)

気付き

Google Colab って便利

ゼロからのPython入門講座 - python.jp では Python 実行環境として Google Colab を使ってます。

Colab とは

Colab(正式名称「Colaboratory」)では、ブラウザ上で Python を記述、実行できます。以下の機能を使用できます。

  • 環境構築が不要
  • GPU に料金なしでアクセス
  • 簡単に共有

Colab は、学生からデータ サイエンティスト、AI リサーチャーまで、皆さんの作業を効率化します。

Google Colab

コンソールとは違って、Web から取ってきた画像は出せるし、Matplotlib を使ったグラフも表示出来る。これ便利ですねえ。

しかしバージョンは

いま最新版が3.10.6で、そろそろ3.11が出るのに3.7.3かあ…

>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=13, releaselevel='final', serial=0)

>>> import platform
>>> platform.python_version_tuple()
('3', '7', '13')

参考:

Python での関数とメソッドの違い

Pythonが提供する機能には、abs() のように 関数 として提供されているものと、 文字列.upper() のように メソッド として提供されているものがあります。特定のデータに強く結びつき、利用頻度が高い処理はメソッドとして使えるようになっており、それ以外の処理は関数として使えるようになっていることが多いようです。

メソッド: ゼロからのPython入門講座 - python.jp

つまりメソッドは

  • 特定のデータに強く結びつく
  • 利用頻度が高い

で関数はそれ以外だと。今のところの印象だと、関数は様々な クラス データ型にまたがる処理を引き受けてる感じがしてる(打ち消し線入れたけど、クラスとデータ型の区別はあるのか無いのかよくわからん)。勘違いかもしれんけど。

リスト list であって配列 Array ではない

名称だけの問題かも知れないけど、Ruby の配列は Python ではリスト。

そして Python の array はリストとはまた別のデータ型。使い方が書いてないので理解できてないが、入れられるデータ型を指定してるようなのでメモリ効率が良いのでしょう。メモリのことを気にする PythonRuby よりも低級言語*1なんだなと感じる。

array --- 効率のよい数値アレイ

このモジュールでは、基本的な値 (文字、整数、浮動小数点数) のアレイ (array、配列) をコンパクトに表現できるオブジェクト型を定義しています。アレイはシーケンス (sequence) 型であり、中に入れるオブジェクトの型に制限があることを除けば、リストとまったく同じように振る舞います。オブジェクト生成時に一文字の 型コード を用いて型を指定します。

array --- 効率のよい数値アレイ — Python 3.10.6 ドキュメント

スライス

Ruby で配列の一部分を抜き出すときには範囲演算子b..e(b <= i <= e)/b...e(b <= i < e)を使うことが多い(個人の見解)。Python では「スライス」なる表記 [b:e:s]で範囲(b <= i < e)と刻み(ステップ s)を表現する。スライスの上限値は 常に含まれない ので(ステップを無視すれば) Ruby の範囲演算子のうちの ... に相当する。 まず Ruby で表現。

> # Pythonに合わせて 10 を使う表現(10.times.to_a)にした。いつもは [*0..9] と書く。
> ten = 10.times.to_a 
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
> ten.size
=> 10
> ten[0..3] # インデックス i が 0 <= i <= 3 を満足する(3を含む)
=> [0, 1, 2, 3]
> ten[0...3] # インデックス i が 0 <= i < 3 を満足する(3を_含まない_)
=> [0, 1, 2]
> ten[5...-2] # インデックス i が 5 <= i < (ten.size -2) を満足する(8を_含まない_)
=> [5, 6, 7]

続いて Python

>>> ten = list(range(10))
>>> print(ten)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> len(ten)
10
>>> ten[0:3] # インデックス i が 0 <= i < 3 を満足する(3を_含まない_)
[0, 1, 2]
>>> ten[5:-2] # インデックス i が 5 <= i < (len(ten) -2) を満足する(8を_含まない_)
[5, 6, 7]

Ruby で普段使っている範囲演算子.. なので Python のスライスは違和感があったのだが、そういえば ... 演算子もあったなあ、ということで納得しました(なんだそりゃ)。

参考:

始値は常に含まれ、終了値は常に含まれない

スライスの使い方をおぼえる良い方法は、インデックスが文字と文字の あいだ (between) を指しており、最初の文字の左端が 0 になっていると考えることです。そうすると、 n 文字からなる文字列中の最後の文字の右端はインデックス n となります。例えばこうです:


>>> word = 'Python'

 +---+---+---+---+---+---+
 | P | y | t | h | o | n |
 +---+---+---+---+---+---+
 0   1   2   3   4   5   6
-6  -5  -4  -3  -2  -1

1行目の数字は文字列の 0 から 6 までのインデックスの位置を示しています; 2行目は対応する負のインデックスを示しています。i から j までのスライスは、それぞれ i と付いた境界から j と付いた境界までの全ての文字から成っています。

3. 形式ばらない Python の紹介 — Python 3.10.6 ドキュメント

スクレイピング

lxml 単体で URL を扱えるのは http だけ

前置き

Pythonスクレイピングするときに一般的に使われるのは Beautiful Soup 4 。ただ、 Beautiful Soup 4 は CSS のみで XPath が使えないらしい(ちゃんと調べてない)。私は以前から XPath を使って HTML を切り刻んでるので Python でも XPath を使いたい。 PythonXML パーサには標準ライブラリとして xml.etree.ElementTree というのがあるようなのだが、遅いのと脆弱性とであまりよろしくないらしい。他方、高速な XML パーサとして lxml がある。「高速」というか C ライブラリの libxml2 のラッパーらしい。

Requests ライブラリ を使わず lxml だけで HTML を取得出来るとの記事があったのだが…

parse()にはURLを直接渡せる

urllibなどを使ってレスポンスをロードしてからlxmlに渡している例が多くありますが、parse()にURLを渡すとそのURLにアクセスして解析してくれます。

 import lxml.html
 tree = lxml.html.parse('http://example.com/')

lxmlでスクレイピングするときのコツ - Regen Techlog (2014-08-06 (最終更新: 2017-07-02))

記事も古いから仕方ないとはいえ…結果は以下。

…だが lxml は https を扱えない orz

というオチなようです。

他の参考リンク:

なので Requests ライブラリか何かで HTML を取ってくる

ということで Requests ライブラリか何かで https の先の HTML を取ってきて、lxml.html.fromstring(html_str) で取得出来る lxml.html.HtmlElement インスタンスxpath メソッドを使ってやればいいようです。

が、文字化けしてるとやはりダメで、下の対処が必要でしたorz ←追記訂正:これは間違った文字コードを前提にした requests.get(url).text で文字化けテキストになったものを lxml.html.fromstring() に渡すからだめなので、バイトコードのまま requests.get(url).contentlxml.html.fromstring() に渡せば問題ない!

Beautiful Soup は「バイト文字列を読み込んで文字コードを推定する機能がある」のと同様に、lxml でもバイトコードであれば文字コードを適切に解釈してくれるようです。ありがたい。

Requests ライブラリで取得した HTML の文字化け

Google Colab で NHK ニュースの新着ニュース一覧の HTML を取得したら日本語部分が文字化けしました(右へスクロールしていってください)。

>>> import requests
>>> url = "https://www3.nhk.or.jp/news/catnew.html"
>>> nhk_news_latest = requests.get(url)
>>> nhk_news_latest.text
<!DOCTYPE HTML>\r\n<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7 eq-ie6"> <![endif]-->\r\n<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8 eq-ie7"> <![endif]-->\r\n<!--[if IE 8]>         <html class="no-js lt-ie9 eq-ie8"> <![endif]-->\r\n<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->\r\n<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#">\r\n<meta charset="utf-8" />\r\n<meta http-equiv="X-UA-Compatible" content="IE=edge" />\r\n<meta http-equiv="X-UA-Compatible" content="requiresActiveX=true" />\r\n<meta name="fragment" content="!" />\r\n\r\n<title>é\x80\x9få\xa0±ã\x83»æ\x96°ç\x9d\x80ä¸\x80覧ï½\x9cNHK NEWS WEB</title>\r\n<meta name="robots" content="noodp,noarchive">\r\n<meta name="keywords" content="é\x80\x9få\xa0±,æ\x96°ç\x9d\x80,ä¸\x80覧,NHK,ã\x83\x8bã\x83¥ã\x83¼ã\x82¹,NHK NEWS WEB" />\r\n<meta name="description" content="NHKã\x81®ã\x83\x8bã\x83¥ã\x83¼ã\x82¹ã\x82µã\x82¤ã\x83\x88ã\x80\x81NHK NEWS WEBã…

<title> タグが <title>é\x80\x9få\xa0±ã\x83»æ\x96°ç\x9d\x80ä¸\x80覧ï½\x9cNHK NEWS WEB</title> のように文字化け。

文字化けの原因

レスポンスヘッダに文字コード情報が記述されていない場合は、デフォルト値のISO-8859-1が設定されてしまいます。

対策

requestsモジュールでは、取得したHTMLに含まれるテキスト情報から、文字コードを推定してくれる機能があります。

apparent_encodingencodingに指定します。

Pythonのrequestsモジュールでの文字コード対策 - かんちゃんの備忘録

に従って encoding をセットしたら文字化けが解消されました(これも右へスクロールしていってください)。

>>> nhk_news_latest.encoding = nhk_news_latest.apparent_encoding
>>> nhk_news_latest.text
<!DOCTYPE HTML>\r\n<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7 eq-ie6"> <![endif]-->\r\n<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8 eq-ie7"> <![endif]-->\r\n<!--[if IE 8]>         <html class="no-js lt-ie9 eq-ie8"> <![endif]-->\r\n<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->\r\n<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#">\r\n<meta charset="utf-8" />\r\n<meta http-equiv="X-UA-Compatible" content="IE=edge" />\r\n<meta http-equiv="X-UA-Compatible" content="requiresActiveX=true" />\r\n<meta name="fragment" content="!" />\r\n\r\n<title>速報・新着一覧|NHK NEWS WEB</title>\r\n<meta name="robots" content="noodp,noarchive">\r\n<meta name="keywords" content="速報,新着,一覧,NHK,ニュース,NHK NEWS WEB" />\r\n<meta name="description" content="NHKのニュースサイト、NHK NEWS WEBの新着ニュースについてのページです。ニュース速報はもちろん、NHK NEWS WEBに掲載されたさまざまなジャンルのニュースを新着順に表示しています。日本と世界の「いま」が分かります。" />\r\n<meta name="copyright" content="NHK(Japan Broadcasting…

<title> タグが <title>速報・新着一覧|NHK NEWS WEB</title> とちゃんと読めるようになりました。

エンコーディングを適切にすると以下のように記事タイトルを取得できました。改めて最初から書いておきます。

>>> import requests
>>> from lxml import html
>>> url = "https://www3.nhk.or.jp/news/catnew.html"
>>> nhk_news_latest = requests.get(url)
>>> nhk_news_latest.encoding = nhk_news_latest.apparent_encoding
>>> nhk_news_latest_doc = html.fromstring(nhk_news_latest.text)
>>> lis = nhk_news_latest_doc.xpath('//ul[@class="content--list grid--col-single"]/li')
>>> [li.xpath('dl/dd/a/em')[0].text for li in lis]
['台風12号 八重山地方の一部が暴風域か 暴風や高波に厳重警戒',
 '神奈川県 新型コロナ 1人死亡 新たに5309人感染確認',
 '千葉県 新型コロナ 11人死亡 新たに3757人感染確認',
 '三重県 新型コロナ 2人死亡 新たに1608人感染確認',
 '茨城県 新型コロナ 5人死亡 新たに1834人感染確認',
 '静岡県 新型コロナ 新たに1923人感染確認',
 '長野県 新型コロナ 新たに1075人感染確認',
 '山梨県 新型コロナ 2人死亡 新たに356人感染確認',
 '富山県 新型コロナ 1人死亡 新たに822人感染確認',
 '愛知県 新型コロナ 2人死亡 新たに5193人感染確認',
 '東京都 新型コロナ 7750人感染確認 前週比1800人余減',
 'コロナ第7波 “死亡者の多くは肺炎以外 容体の傾向が変化”',
 '栃木県 新型コロナ 1人死亡 新たに972人感染確認',
 '北海道 新型コロナ 2人死亡 新たに3295人感染確認',
 '長崎県 新型コロナ 3人死亡 新たに610人感染確認',
 '岐阜県 新型コロナ 2人死亡 新たに1364人感染確認',
 '大分県 新型コロナ 新たに706人感染確認',
 '広島県 新型コロナ 2人死亡 新たに2604人感染確認',
 '山口県 新型コロナ 2人死亡 新たに885人感染確認',
 '沖縄県 新型コロナ 新たに721人感染確認']

なおニュースは2022/9/11 17:20現在の記事。

でもね。上にも書いたけど requests.get(url).content にすれば文字化けせずに済む

もう一度書きます。requests.get(url).text で文字化けテキストになったものを lxml.html.fromstring() に渡すからだめなので、バイトコードのまま requests.get(url).contentlxml.html.fromstring() に渡せば問題ない!

requests.get を使ってデータを含むウェブページを取得し、 html モジュールを使って解析し、結果を tree に保存します:


page = requests.get('http://econpy.pythonanywhere.com/ex/001.html')
tree = html.fromstring(page.content)

page.text ではなく page.content を使用する必要があります。なぜなら、 html.fromstring は入力として bytes を暗黙的に期待しているからです。)

lxml と Requests — HTMLスクレイピング — The Hitchhiker's Guide to Python

バイトコードを返す requests.get(url).content を使えば、encoding をセットしなくても最終的に文字化けしません!ネットでは requests.get(url).textだらけだけど、みんな requests.get(url).content を使おう!

>>> nhk_news_local = requests.get("https://www3.nhk.or.jp/lnews/")
>>> nhk_news_local_doc = html.fromstring(nhk_news_local.content)
>>> local_lis = nhk_news_local_doc.xpath('//ul[@class="content--list grid--row-wide"]/li')
>>> [li.xpath('a/dl/dd/em')[0].text for li in local_lis]
['カピバラの赤ちゃん すくすく成長 栃木 那須町',
 '彦根城 夜間特別公開が開始 中秋の名月眺める 滋賀 彦根',
 '伝統芸能の「備中神楽」 中秋の名月のもと楽しむ 岡山 高梁',
 '「恐竜の着ぐるみレース」200mの特設コースを疾走 福井 勝山',
 '西九州新幹線試乗会 最新鋭車両が佐賀 武雄温泉~長崎を走行',
 '【動画】小学生が捕獲「バナナウナギ」水族館で展示 三重 伊勢',
 '【動画】10頭の母パンダ「良浜」22歳誕生日 和歌山 白浜町']

NHKローカルニュースは2022/9/12の記事。

オブジェクトの持つ属性だのメソッドだのを知りたい

関数 dir()

関数 vars()

dir()vars() の違い

(dir()では)オブジェクトが持つ属性のみならず、オブジェクトが属しているクラスが持つ属性をも含んだリストが返ってきます。

Pythonのvars()とdir()の違い - minus9d's diary

inspect モジュールのメソッド inspect.getmembers()

不満

クラス、メソッド、データ属性

クラスとは何ですか?

クラスは、class 文の実行で生成される特殊なオブジェクトです。クラスオブジェクトはインスタンスオブジェクトを生成するためのテンプレートとして使われ、あるデータ型に特有のデータ (attribute/属性) とコード (メソッド) の両方を内蔵しています。

メソッドとは何ですか?

メソッドは、オブジェクト x が持つ関数で、通常 x.name(arguments...) として呼び出されるものです。メソッドはクラス定義の中で関数として定義されます:

オブジェクト — プログラミング FAQ — Python 3.10.6 ドキュメント

以前に JavaScript を学んだときにも感じた違和感。Rubyist はデータ属性(アトリビュート)とメソッドとを区別してない。だってクラス内で内部処理があろうが無かろうが(つまりメソッドだろうが属性値だろうが)関係なく「返値」として取り扱ってる。メソッドには引数があることもあるし無い事もある。そして Ruby だと引数の無いメソッドは属性値とほぼ変わりないので、メソッド名のあとにメソッドである印としての括弧 () が存在しない。Python 同様 JavaScript でもやはりメソッドの後ろには(引数が無くても)括弧を付けなきゃいけない。なんだか面倒。でもこれは文法だから従わないといけない。

メンバって何よ

名称の話なのだが「メンバ」という単語がある。上記の「クラスとは何ですか?」での「データ (attribute/属性)」に相当するようである。どうやらC++ 由来の用語らしい。

C++ の用語で言えば、通常のクラスメンバ (データメンバも含む) は (プライベート変数 に書かれている例外を除いて) public であり、メンバ関数はすべて 仮想関数(virtual) です。 Modula-3 にあるような、オブジェクトのメンバをメソッドから参照するための短縮した記法は使えません: メソッド関数の宣言では、オブジェクト自体を表す第一引数を明示しなければなりません。第一引数のオブジェクトはメソッド呼び出しの際に暗黙の引数として渡されます。

9. クラス — Python 3.10.6 ドキュメント

「メンバ」なる用語が気になったのは、オブジェクト(インスタンス)の持ってる属性値やメソッド名を調べるときに、関数 dir() の他に inspect.getmembers() というメソッド*2がある、と知ったとき。

dir()関数以外で、inspectという標準ライブラリを使う方法もあります。

getmembers()を使うと属性が取得できるようだ。

標準ライブラリinspectで調べる - 【dirとinspect】Pythonライブラリの属性、メソッド一覧を調べる方法 - よちよちpython

このメソッドの名称が「ゲットメンバーズ」。んじゃ「メンバ」って何よ?って事になったわけで。

しかも、なんとなく「メンバ」が属性値っぽいよなーと思っていつつも、実際に inspect.getmembers() の返値を見ると属性値のほかにメソッドも含まれているし、なんかよくわからん。

徐々に慣れていくしかないのであろう。

なお inspect.getmembers() に関する情報の役立ちリンク:

関数とメソッド、文の可読性

Ruby の場合は「何もかも全てオブジェクト、関数のように見えてもメソッド」という極端にオブジェクト指向な言語です。そして「返値はどの式にも存在する、メソッドチェーンで続けていくのが快感」という点もやはりクセあります*3

例えばこんな感じでメソッドチェーンを続けていくのが Rubyish(個人の意見です)。

> "Ruby_haS_MeThod_chAin_cOnTinuE_InFinite.".split("_").map { |word| word.downcase }.join(" ").capitalize
=> "Ruby has method chain continue infinite."

そして素人に毛が生えた程度の知識で上と同じことを Python で書いてみた。「内包表記」ってやつを使った。

>>> " ".join([word.lower() for word in "Ruby_haS_MeThod_chAin_cOnTinuE_InFinite.".split("_")]).capitalize()
'Ruby has method chain continue infinite.'

可読性を上げようと思えばいくらでも上げられるのでしょうが、現状では英文を日本語訳するかのような「一文の中で前へ行ったり後ろへ行ったり」感がとても強くて読みづらい。

Ruby に慣れていると、インスタンスの処理を後ろにドットで続けていくことが「見やすい」と感じるため、Python での「関数として引数の前に関数名が来る」事、そしていちいち括弧を付けなきゃいけない(というか括弧が無い事での可読性の低下の激しい)こと、それに加えてメソッド名がインスタンスの後ろに来る事による(上にも書いたけど)あっち行ったりこっち行ったり感に違和感を強く感じています。関数しか使わないのであれば H(G(F(x))) みたいに順々に前へ読んでいくので一方向なのだけど。

他方、括弧の使わなさに関しては Ruby が極端なのですけど。

",".join(["a", "b", "c"])"a,b,c".split(",") の(ある種の)対称性

Ruby だと配列 ["a", "b", "c"], で結びつけるのは Array#join メソッドを使った

["a", "b", "c"].join(",")

であり、逆に文字列 "a,b,c", で区切って行列にするのは String#split メソッドを使った

"a,b,c".split(",")

とするわけです。が、Python では前者が文字列メソッド

",".join(["a", "b", "c"])

になっています。Rubyish な脳では真っ先に拒絶反応が生じたのがこのメソッド。

「なんでコレクションに対するメソッドじゃなくて文字列メソッドなのよ!?」

この疑問を感じるのはどうやら Rubyist だけではないようで、Python ドキュメントの FAQ にも取り上げられています。

…一部のプログラマに不快を感じさせていると思われる…

「文字列リテラル (文字列定数) のメソッドを使うのは醜すぎる」

「私は実際、要素を文字列定数とともに結合させるよう、シーケンスに命じているのだ」

そしてこれらの不満への返答が ↓ です。

join() は、セパレータ文字列に、文字列のシーケンスをイテレートして隣り合う要素の間に自身を挿入するように指示しているので、文字列のメソッドです。このメソッドは、独自に定義された新しいクラスを含め、シーケンスの規則を満たすいかなる引数にも使えます。

納得は出来ない*4のですが、ココだけを我慢すればまだ何とかなるかなあと思っています。

学習元

*1:最近はどうやら「低水準言語」と言うようだ

*2:用語は「メソッド」で良いんだよね??

*3:他の言語はほとんど触ってないので実はよく分かってませんが

*4:美的センスに欠けると感じる