───解放しろ、全てを。

初めてBeautifulSoupでWebスクレイピングして得たTips、ハマったこと

新人研修ぶりにコード書いた

Tips

公式ドキュメントを読む

Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation

近藤茂徳さんという神がほぼ完全に訳してくれています。

kondou.com - Beautiful Soup 4.2.0 Doc. 日本語訳 (2013-11-19最終更新)

GoogleSpreadSheet連携便利

おまじないの部分だけ書いときます。

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# https://docs.google.com/spreadsheets/d/シートid/edit#gid=0
sh = gc.open_by_key('シートid')
# 先頭のシートを開く
worksheet = sh.get_worksheet(0)

要素の塊でとると楽っぽい

soup.find("table tr:nth-of-type(2) td:nth-of-type(1)")
soup.find("table tr:nth-of-type(3) td:nth-of-type(1)")
soup.find("table tr:nth-of-type(4) td:nth-of-type(1)")

こんな感じでやってたけど、どう考えてもtableで取ってきた後に相対的にtd取った方が楽に運用できる。

上記の例だとhtml変わるとめんどい。余計な変数も減る。

soup_table = soup.find('table')
soup_table.find("tr:nth-of-type(2) td:nth-of-type(1)")
soup_table.find("tr:nth-of-type(3) td:nth-of-type(1)")
soup_table.find("tr:nth-of-type(4) td:nth-of-type(1)")

find_all()はリストを、find()はbs4.element.Tagを返す

print(type(soup.find_all('td')))
print(type(soup.select('td')))
print(type(soup.find('td')))
print(type(soup.select_one('td')))

# <class 'bs4.element.ResultSet'>
# <class 'list'>
# <class 'bs4.element.Tag'>
# <class 'bs4.element.Tag'>

と思ったら違ったわ。bs4.element.ResultSet!?誰だお前!!!!!!

1個取りたいならfind()かselect_one()

print(html.select("table tr:nth-of-type(2) td:nth-of-type(1)"))
# ['解放しろ']

print(html.select_one("table tr:nth-of-type(2) td:nth-of-type(1)"))
# 解放しろ

td:nth-of-type(1)は1個しかないので、select()よりselect_one()の方が扱いやすい。

リストイラナイ

bs4.element.ResultSet!?!?!誰だお前!?!?!!?!?!

タグの表記揺れはない

BeautifulSoupで持ってきたhtmlのタグは最適化されるっぽい。

html="解放<br>しろ、<br/>全て<br />を。"

print(BeautifulSoup(html, 'lxml'))
# <html><body><p>解放<br/>しろ、<br/>全て<br/>を。</p></body></html>

<br>, <br/>, <br />が全部<br/>になってるね。あとhtmlとbodyとpも自動的に付加されてる。

例えば、brタグの削除に複数条件指定しなくてもいい。

#正規表現モジュールreを用いたタグ除去
re.split("<br/>|<br />", "")

こんな風にしてたけど消すのめんどいからそのままにしてるはw

変数同時宣言

初期化に便利。

elm1 = ""
elm2 = ""

elm1 = elm2 = ""

ただし、これは後にハマる。

▶︎リストの複数宣言はできない。

x, y, z = 1, 2, 3

print(x, y, z)
# 1 2 3

1行で書けるこれも楽。

リスト作るならリスト内包表記が便利説

x, y, z = 1, 2, 3

int_list= []
for i in [x, y, z]:
    int_list.append(i)
print(int_list)
# [1, 2, 3]

print([i for i in [x, y, z]])
# [1, 2, 3]

リストがいっぱいあると初期化がめんどくなる。

リスト内包表記なら3行が1行に。

if文を1行にする: 三項演算子

条件式が真の時の処理 if 条件式 else 条件式が偽の時の処理

下記x > 1ならy = "解☆放"、それ以外ならy = "未解放…"

x = 2
y = "解☆放" if x > 1 else "未解放…"

print(y)
# 解☆放

▲と▼は等価。

x = 2
if x > 1:
    y = "解☆放"
else:
    y = "未解放…"

print(y)
# 解☆放

三項演算子の中に三項演算子を使えばelifも書ける。

x = 0
y = "解☆放" if x > 1 else "わたしは1です" if x == 1 else "未解放…"

print(y)
# 未解放…

上述のリスト内包表記と併用すると便利。

要素のテキストを取得する: text, string

BeautifulSoupではgetText()が用意されてるけど、下記でもテキストだけ取得できる。

text1 = BeautifulSoup('<p>解放しろ、全てを</p>')
text2 = BeautifulSoup('<p><b>解放しろ</b>、全てを</p>')

print(text1.text)
print(text1.string)
print(text2.text)
print(text2.string)

# 解放しろ、全てを。
# 解放しろ、全てを。
# 解放しろ、全てを。
# None

stringだと子要素が複数ある時にNoneが返されるので注意。

改行コード削除: replace(), splitlines()

html = '''
解放しろ、
全てを。
'''
soup = BeautifulSoup(html, 'lxml')

print(soup.text)
# 解放しろ、
# 全てを。

print(soup.text.replace("\n", ""))
# 解放しろ、全てを。

print(soup.text.splitlines())
# ['解放しろ、', '全てを。']

print(soup.text.split())
# ['解放しろ、', '全てを。']

改行コード\nreplace()で空文字に置換することで削除できます。

splitlines()は改行で区切ったリストが返される。

一番下のsplit()でも似たようなことができますが、こちらはスペースでも区切られるので注意。

文字列削除: スライス機能

txt ="ReleaseAll"

print(txt[:3]) #先頭から3文字
print(txt[-3:]) #末尾から3文字
print(txt[3:]) #3文字目から末尾
print(txt[:-3]) #先頭から末尾3文字目

# Rel
# All
# easeAll
# Release

分からなければ下記記事参照。

マルチカーソル便利すぎる

Python関係ないけどAtomでマルチカーソルが使えるsequential-numberが便利すぎる。

Atomの設定 > インストール > sequential-numberと検索。インストール。

option + ドラッグで行を選択。

control + option + 0sequential-numberの入力パネルを開く。

sequential-numberで入力する構文は以下

<開始番号> <足し算or引き算> <加算する数値> : <最小桁数>

開始番号だけでもそこからの連番が作れる。

Atomでマルチカーソルの連番入力をするパッケージ「sequential-number」をリリースしました | WebDesign Dackel

ハマったこと

brタグで分割されたデータの取得

解決方法はいくつかあるが、文字列にしてsplit()が楽そう。

html = '''
<table>
    <tr>
        <td>
            解放しろ
            <br/>
            全てを。
        </td>
    </tr>
</table>
'''
soup = BeautifulSoup(html, 'html.parser')

print(soup.find('td').text.split())
# ['解放しろ', '全てを。']

ただし、間にスペースやタグがある場合は上記だと正しく取得できない。詳細は下記ページ。

select()で:nth-child()使えない

select()の引数で:nth-child()は使えない。

ハマったわけではないけど、:nth-child()から:nth-of-type()に鞍替えしました。

Google ColaboratoryでGoogleスプレッドシートを読み書きしてみる - uepon日々の備忘録

リストの複数宣言はできない。

list1 =  [1, 2, 3]
list2 =  [1, 2, 3]

これを1行で記述しようとして▶︎変数同時宣言のように記述すると、

list1 = list2 = [1, 2, 3]

print(id(list1))
print(id(list2))

# 140051905329480
# 140051905329480

これだとlist1list2は同じオブジェクトになるので、list1の値を更新するとlist2も更新されてしまう。

list1[0] = 4

print(list1)
print(list2)

# [4, 2, 3]
# [4, 2, 3]

リスト宣言が面倒な場合はリスト内包表記を使う。

print([i for i in [1, 2, 3]])
# [1, 2, 3]

リスト内包表記なら、宣言と処理を1行で表記できる。

複数のin演算子を一つにまとめる: any()

解放しろ、全てをの文字列中にまたはがあった場合に真を返したい。

下記では正しく動かない。

if "全" or "善" in "解放しろ、全てを":
    print("まる")
    # まる

まるが返されるので一見正しく見えるけど、"全" or "善"じゃなくてもまるになる。

"あ" or "い" in "解放しろ、全てを"もまる。

そこで、論理和が求められるany()を使う。

引数はliterableオブジェクト。

if any(i in ("全", "善") for i in "解放しろ、全てを"):
    print("まる")

# まる

参考記事