質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.09%

テーブルのスクレイピングで値のないカラムにも値をセットしてcsv出力したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 285

score 46

初めてスクレイピングをしようとしています。
色々なサイトを参考に何とか動くようにはなりましたが、想定通りの結果にならず躓いています。
よろしくお願いします。

前提・実現したいこと

あるサイトからテーブルの内容をスクレイピングし、csv出力したいです。
リンク内容
テーブルの一部カラムが欠損しているのがネックなようで思ったような取得結果になりません。

サイトには下記のようなテーブルが複数あります。
同じ機械の場合、機械欄はブランクとなっています。

機械 メーカー 数量
機械A abc 1
機械B bbb 1
ccc 3
ddd 2
eee 5
fff 1

最終的に取得したい結果は下記のようなcsvです

機械,メーカー,数量
機械A,abc,1
機械B,bbb,1
機械B,ccc,1
機械B,ddd,2
機械B,eee,5
機械B,fff,1

発生している問題・エラーメッセージ

試してみたところ、こんな形式で出力されてしまいました。

機械種別,メーカー,数量,機械A,abc,1,機械B,bbb,1,ccc,1,ddd,2,eee,5,fff,1

該当のソースコード

from bs4.element import Tag
import requests
from bs4 import BeautifulSoup
import csv

url = 'https://xxxxxxx'
# リクエスト
res = requests.get(url)    
# エンコーディング
res.encoding = res.apparent_encoding
# htmlをパース
soup = BeautifulSoup(res.text, "html.parser")
# データ取得
tables = soup.find_all(class_="tbl-data-01") 
file = 'C:/xxxxxxx/result.csv'
with open(file, "a", encoding='utf-8') as file:
    writer = csv.writer(file)
    for table in tables:
        rows = table.findAll("tbody")        
        for row in rows:
            csvRow = []
            for cell in row.findAll(['td', 'th']):
                print(cell.text)
                csvRow.append(cell.get_text())
            if len(csvRow) > 0:
                writer.writerow(csvRow)

試したこと

上記のコードの前に下記でやってみたところ、各機械の1行目しか取れませんでした。
HTMLのソースを眺めていてtbodyfind_allした方が良さそうだと検討をつけて
上記のコードになりました。

from bs4.element import Tag
import requests
from bs4 import BeautifulSoup
import csv

url = 'https://xxxxxxx'
# リクエスト
res = requests.get(url)    
# エンコーディング
res.encoding = res.apparent_encoding
# htmlをパース
soup = BeautifulSoup(res.text, "html.parser")
tables = soup.find_all(class_="tbl-data-01") 
file = 'C:/xxxxxxx/result.csv'
with open(file, "a", encoding='utf-8') as file:
    writer = csv.writer(file)
    for table in tables:
        #print(table.tbody)
        rows = table.findAll("tr")        
        for row in rows:
            csvRow = []
            for cell in row.findAll(['td', 'th']):
                print(cell.text)
                csvRow.append(cell.get_text())
            if len(csvRow) > 0:
                writer.writerow(csvRow)

このコードで取得できたのが下記の結果です

機械,メーカー,数量
機械A,abc,1
機械B,bbb,1

補足情報(FW/ツールのバージョンなど)

python 3.8.1
vscode 1.48.2

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

どの様なソースかわかりませんが、以下の様にすれば
実現が可能になるのではないでしょうか。
メーカー名だけを保持する変数を用意し
もしもメーカー名が空だった場合には
そのまま前度のメーカー名が補填するという流れで再現が可能かと思います。

(本質問ページ上のtable要素を対象にテストしたものであり、
質問者様のスクレイピングを行おうとしているページにそのまま流用できるかは分かりかねます)

from bs4 import BeautifulSoup
import requests
import pandas as pd

header = [th.text for th in tables.find_all('th')]
result = []
for tr in tables.find('tbody').find_all('tr'):
    tmp = [td.text for td in tr.find_all('td')]
    if tmp[0] != '':
        maker = tmp[0]
    else:
        tmp[0] = maker
    result.append(tmp)

df = pd.DataFrame(result)
df.to_csv('result.csv', encoding='cp932', header=header, index=False)

追記

とりあえずは以下で目的の形での取得が可能かと思います。
ちょっと複雑で説明が難しいです。
for文ではenumerarteで何番目かを判定しています。
0番目(初回)のループである場合にはヘッダー行は無視し
それ以外の場合には要素をtd要素を抽出します。

今回のソースの場合[機械種別]が記載されたtrにrowspanが設定されていた為
rowspanを抽出し、rowspanが1を超える場合には
.find_next_siblings()で兄弟要素を取得し
先頭列に[機械種別]の記載がないデータの数(列(rowspan-1)×2行)分のデータをスライスして抽出しています。
抽出したデータの先頭に保持していたgenre変数(機械種別名)をおいてデータを整形し
都度resultリストに追加していき、最後にdfに変換するという流れです。

from bs4 import BeautifulSoup
import requests
import pandas as pd

url = 'https://ja.nc-net.or.jp/search/equipment/?cl[]=1'

res = requests.get(url)
soup = BeautifulSoup(res.content, 'html.parser')
tables = soup.find('table', id='equip1_list')

#print(tables.prettify())
header = [th.text for th in tables.find_all('th')]
result = []
for e, tr in enumerate(tables.find('tbody').find_all('tr')):
    if e != 0:
        genre, maker, num = [td.text for td in tr.find_all('td')]
        span = int(tr.find_all('td')[0].get('rowspan'))
        result.append([genre, maker, num.replace('台', '')])

        if span > 1:
            after = [td.text for td in tr.find_next_siblings()[0: (span-1)*2]]
            for i in range(0, len(after), 2):
                maker, num = after[i: i+2]
                result.append([genre, maker, num.replace('台', '')])


df = pd.DataFrame(result)
df.to_csv('result.csv', encoding='cp932', header=header, index=False)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/12/02 17:03

    特殊なソースなんですね。。。
    htmlは全くの門外漢で今回初めてやることになったので、そこらへんのソースの特殊性等に気がつかないままでした。
    わからないながらも悪戦苦闘して種別に値のあるものだけしか取れないか、同一種別が複数あるものが取れてもサンプルであげたようにだらだらと1行で取れてしまうかです。
    もし、何か良いお知恵があれば引き続きおつきあい頂けるとありがたいです。

    キャンセル

  • 2020/12/02 19:27

    先ほど回答の追記を致しました。
    完全にお手伝いとなってしまい、また解説もやや難しくなってしまいましたが
    どうかじっくり解説やコードの流れを理解していただけると幸いです。

    キャンセル

  • 2020/12/03 10:27

    想定通りの結果が出力できました。
    enumerarteやfind_next_siblings()の使い方を知ることができました。
    調べても、ここら辺を探し出すことができなかったのでなるほどと思いました。
    実際に動かしてみて値がどのように取れていくのか確認したことで、解説ではよくわからなかった
    「データの数(列(rowspan-1)×2行)分のデータをスライス」の部分も理解できました。
    全コードご提示いただくことになってしまいましたが、大変勉強になりました。
    どうもありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.09%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る