# PythonによるWebサイトの自動検索-2
# 国土交通省の「ネガティブ情報等検索サイト」から、一定期間に行政処分を受けた建設業者の情報を収集し、Excelに保存する
# 必要なモジュールを呼び出す
import requests
from bs4 import BeautifulSoup
from datetime import datetime, date, time , timedelta
from dateutil.relativedelta import relativedelta
import os
import pandas as pd
from openpyxl import load_workbook
import re # ★
# 検索結果から情報を抽出したときに格納するリスト
rs_url = [] # ネガティブ情報の「処分詳細URL」
rs_syobun = [] # ネガティブ情報の「処分の内容(詳細)」
rs_riyuu = [] # ネガティブ情報の「処分の原因となった事実」⇒「処分理由」と呼ぶ
rs_gyouhou = [] # ★処分理由が「建設業法違反」の場合の違反となった規定名
rs_sonota = [] # ★処分理由が「その他法令違反」の場合の違反をした法令名
# ★建設業法違反の判定フレーズリスト [x0,[x1],x2] ⇒「処分理由」にx0とx1が共に含まれていれば x2の建設業法違反があったものとみなす
# ★( X0 1つに対し、x1 は二次元リストの形式で複数のフレーズを指定可能)
x = [['一括',['請'],'一括下請負禁止に関する違反'], # ★
['公衆',['危害','災害'],'公衆災害'],
['施工体制',['偽'],'施工体制台帳に関する違反'],
['施工体系',['偽'],'施工体制台帳に関する違反'],
['再下請',['通知','申請'],'施工体制台帳に関する違反'],
['管理責任者',['許可','配置','届出','欠'],'建設業許可要件に関する違反'],
['所在',['確知','確認','公告'],'建設業許可要件に関する違反'],
['許可',['欠格','偽','更新','届出'],'建設業許可要件に関する違反'],
['役員',['懲役','禁錮'],'建設業許可要件に関する違反'],
['代表者',['死亡'],'建設業許可要件に関する違反'],
['欠格',['事由','暴力団'],'建設業許可要件に関する違反'],
['建設業',['解散'],'建設業許可要件に関する違反'],
['破産',['開始','決定'],'建設業許可要件に関する違反'],
['経営規模',['偽'],'経営事項審査に関する違反'],
['計算書',['偽'],'経営事項審査に関する違反'],
['経営事項',['偽','違反','申請'],'経営事項審査に関する違反'],
['経営事項',['競争','入札','受注'],'入札参加資格審査に関する違反'],
['競争参加',['不正','偽','要件'],'入札参加資格審査に関する違反'],
['入札参加',['不正','偽','要件'],'入札参加資格審査に関する違反'],
['入札',['名義'],'不誠実な入札'],
['契約書',['作成'],'契約書作成義務に関する違反'],
['契約',['書面'],'契約書作成義務に関する違反'],
['技術者',['許可','専任','置','不在','勤務'],'技術者配置義務に関する違反'],
['専任',['置','不在','勤務'],'技術者配置義務に関する違反'],
['施工',['不適合','瑕疵','粗雑'],'不誠実な施工'],
['工事',['不適合','瑕疵','粗雑'],'不誠実な施工'],
['特定建設業',['下請契約'],'一般建設業者限度額に関する違反'],
['許可',['下請契約','金額以上','軽微'],'無許可業者限度額に関する違反'],
['下請',['金額以上','軽微'],'無許可業者限度額に関する違反'],
['許可',['受け'],'建設業許可要件に関する違反'],
['変更',['偽','欠'],'建設業許可要件に関する違反'],
['建設業法',['経過措置','罰金'],'建設業法違反']
]
# ★その他の法令違反の判定フレーズ [y0,y1]⇒「処分理由」にy0が含まれていれば y1の法令違反があったものとみなす
# ★( y0 はワイルドカード「.*」を指定できるようにしている)
y = [['刑法','刑法違反'], # ★
['独占.*禁止.*法','独占禁止法違反'],
['税.*法','税法違反'],
['会社法','会社法違反'],
['安全衛生法','労働安全衛生法違反'],
['廃棄物.*処理.*法','廃棄物処理法違反'],
['海洋汚染.*防止.*法','海洋汚染防止法違反'],
['建築基準法','建築基準法違反'],
['住宅瑕疵.*履行.*法','住宅瑕疵担保履行法違反'],
['労働者派遣.*法','労働者派遣法違反'],
['出入国管理.*法','出入国管理法違反'],
['道路交通法','道路交通法違反'],
['自動車.*運転.*処罰.*法','自動車運転処罰法違反'],
['船舶安全法','船舶安全法違反'],
['公職選挙法','公職選挙法違反'],
['覚.*剤取締法','覚醒剤取締法違反'],
['大麻取締法','大麻取締法違反'],
['貸金業法','貸金業法違反'],
['盗犯.*防止.*法','盗犯等防止法違反'],
['銃刀.*所持.*法','銃刀法違反'],
['特定商取引.*法','特定商取引法違反'],
['条例','条例違反'],
['公契約.*競売.*妨害','刑法違反(公契約競売入札妨害罪)'],
['公の入札.*刑','刑法違反(公契約競売入札妨害罪)'],
['贈賄','刑法違反(贈賄罪)'],
['賄賂','刑法違反(贈賄罪)'],
['傷害','刑法違反(傷害罪)'],
['暴行','刑法違反(暴行罪)'],
['詐欺','刑法違反(詐欺罪)'],
['業務上過失.*罪','刑法違反(業務上過失致死傷罪)'],
['労働.*災害.*刑','労働安全衛生法違反'],
['労働者.*危険.*防止','労働安全衛生法違反'],
['過失運転.*罪','自動車運転処罰法違反'],
]
# 検索結果を出力するディレクトリ「dir」を指定する
dir = r'C:\Users\......\......\......\......\......'
# 「ネガティブ情報」を検索する検索開始月と検索期間(月数)を設定する
kaisi = '0' # 検索開始月
while len(kaisi) != 6:
kaisi = input('「ネガティブ情報」を検索する開始月を数字6桁で入力してください yyyymm 形式 :')
kikan = 0 # 検索期間(月数)
while kikan<1 or kikan>6:
kikan = int(input('「ネガティブ情報」を検索する期間(月数)を1~6の範囲で入力してください :'))
# 検索に利用する様々な日付を作成する
fy = kaisi[0:4] # 検索開始年
fm = str(int(kaisi[4:6])) # 検索開始月
fmz = fm.zfill(2) # 検索開始月が1桁の場合は前ゼロを付けて2桁に揃える
ft = date(int(fy),int(fm),1) # 検索開始年月(1日)
tt = ft + relativedelta(months=kikan-1) # (kikan-1)月後の年月(1日)
ty = str(tt.year) # 検索終了年
tm = str(tt.month) # 検索終了月
tmz = tm.zfill(2) # 検索終了月が1桁の場合は前ゼロを付けて2桁に揃える
# 「ネガティブ情報等検索サイト」のドメインのURL「url0」を指定する
url0 = 'https://www.mlit.go.jp/nega-inf/cgi-bin/'
# 「ネガティブ情報等検索サイト」の検索条件を入れたURL「url」を生成する
url = (url0 + 'search.cgi?'
+'jigyoubunya=kensetugyousya' # 建設業者
+'&'+'EID=search'
+'&'+'start_year='+fy+'&'+'start_month='+fm # 検索開始年月
+'&'+'end_year='+ty+'&'+'end_month='+tm # 検索終了年月
+'&'+'disposal_name1='+'&'+'disposal_name2='
+'&'+'reason_con=1'
+'&'+'reason1='+'&'+'reason2='+'&'+'reason3='
+'&'+'shobun='
+'&'+'address='
+'&'+'agency='
)
# 「url」の検索リクエストを送り、検索結果のうち総件数を「r_ken」に格納し、数値としての総件数を「n」とする
r = requests.get(url)
r.raise_for_status()
soup = BeautifulSoup(r.content,'html.parser')
r_ken = soup.find_all(class_='title')
n = int(r_ken[0].text.replace('検索結果:','').replace('件',''))
# 検索結果は、表形式で表示されるので、データフレーム形式で収集して「df」にまとめていく
flag = '0'
p = 0 # 検索結果のページ番号
# 1ページ10件ずつ表示されるので、総件数がn件になるまで「url」にページ番号を付け10件ずつ検索結果を呼び出して収集する
while flag != '1':
p += 1 # ページ番号「p」を1つ増やす
urls = url+'&'+'page='+str(p) # 「url」にページ番号を付けた「urls」を生成する
dfm = pd.read_html(urls,header=0,index_col=None) # 表形式の検索結果を「dfm」に格納する
if p == 1:
df = dfm[0] # 1ページ目(最初の10件)は「dfm」をそのまま「df」とする
else:
df = pd.concat([df,dfm[0]]) # 2ページ目(次の10件)以降は「dfm」を「df」に追記する
# 「urls」の検索リクエストを送り、検索結果のうち「処分詳細」欄にリンクされているURLの情報を10件まとめて「rs_urls」に格納する
r = requests.get(urls)
r.raise_for_status()
soup = BeautifulSoup(r.content,'html.parser')
rs_urls = soup.find_all(class_='overview')
# 「rs_urls」に含まれるURL情報を1件ずつ取り出し、ドメインを付加して「処分詳細URL」の「rsg_url」として生成し、「rs_url」にリスト化する
for i in range(len(rs_urls)):
rsg_url = url0 + rs_urls[i].get('href') # ドメイン「url0」に属性名「href」で取得したディレクトリを付加
rs_url += [rsg_url] # 「処分詳細URL」のリスト
# 「rsg_url」により表示される詳細情報は表形式だが、データフレームとしては適さない形なので、
# 「rsg_url」の検索リクエストを送り、検索結果のうち「処分詳細」の内容を1件分まとめて「rs_texts」として取得し、
# そのうちテキスト部分を「rs_text」として取得する
rs = requests.get(rsg_url)
rs.raise_for_status()
soup = BeautifulSoup(rs.content,'html.parser')
rs_texts = soup.find_all(class_='overview__list')
rs_text = rs_texts[0].text.replace(' ','').replace(' ','') # ★
# 「rs_text」のうち「処分の内容(詳細)」を切り分けて「rs_syobun」としてリスト化する
t1 = rs_text.find('処分の内容(詳細)')+len('処分の内容(詳細)') # 「処分の内容(詳細)」開始位置
t2 = rs_text.find('処分の原因となった事実') # 「処分の内容(詳細)」終了位置の1つ先
rs_syobun += [rs_text[t1:t2]] # 「処分の内容(詳細)」のリスト
# 「rs_text」のうち「処分の原因となった事実(=処分理由)」を切り分けて「rs_riyuu」としてリスト化する
t3 = t2+len('処分の原因となった事実') # 「処分の原因となった事実」開始位置
t4 = rs_text.find('その他参考となる事項') # 「処分の原因となった事実」終了位置の1つ先
rg_riyuu = rs_text[t3:t4] # ★
rs_riyuu += [rg_riyuu] # ★「処分理由」のリスト
# ★「処分理由」にその他の法律違反の判定フレーズ(y0)があれば y1を「rs_sonota」としてリスト化する
rg_sonota = '-' # ★
for j in range(len(y)): # ★
reg = re.compile(y[j][0]) # ★
if reg.search(rg_riyuu) != None: # ★
rg_sonota = y[j][1] # ★
break # ★
rs_sonota += [rg_sonota] # ★「その他の法律違反」のリスト
# ★「処分理由」にその他の法令違反の判定フレーズ(y0)がなかった場合は、
# ★「処分理由」に建設業法違反の判定フレーズ(x0 かつ x1)があれば x2を「rs_gyouhou」としてリスト化する
rg_gyouhou = '-' # ★
if rg_sonota == '-': # ★
for j in range(len(x)): # ★
if x[j][0] in rg_riyuu: # ★
for k in range(len(x[j][1])): # ★
if x[j][1][k] in rg_riyuu: # ★
rg_gyouhou = x[j][2] # ★
break # ★
else: # ★
continue # ★
break # ★
rs_gyouhou += [rg_gyouhou] # ★「建設業法違反」のリスト
# 「df」の行数が総件数「n」になった段階で while~文を脱する
if df.shape[0] >= n:
flag = '1'
# 「df」から「処分詳細」の列を削除する(リンク情報を取り出した後は有益な情報がないため)
df = df.drop('処分詳細',axis=1)
# ★「df」に新しい列として「処分の内容(詳細)」「処分理由」「建設業法違反」「その他法令違反」「詳細URL」を設け、収集してリスト化した各データを追加する
df.insert(5,'処分の内容(詳細)',rs_syobun,True)
df.insert(6,'処分理由',rs_riyuu,True)
df.insert(7,'建設業法違反',rs_gyouhou,True) # ★
df.insert(8,'その他法令違反',rs_sonota,True) # ★
df.insert(9,'詳細URL',rs_url,True) # ★
# 「df」をExcelファイルとして出力する
nam = '国交省ネガティブ情報'+fy+fmz+'~'+ty+tmz+'.xlsx'
path = os.path.join(dir,nam)
df.to_excel(path,sheet_name='建設業者',index=False)
# Excelシートの列幅を修正する
col = [['A',20], # 列番号(アルファベット)と列幅の組合せをリスト化
['B',12],
['C',14],
['D',14],
['E',10],
['F',20],
['G',20],
['H',20],
['I',20], # ★
['J',20] # ★
]
wb = load_workbook(path)
ws = wb.active
for i in range(len(col)):
ws.column_dimensions[col[i][0]].width = col[i][1]
wb.save(path)
# 実行が成功したら結果を画面表示する
print('パス '+path+' でファイルを保存しました')