# 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+' でファイルを保存しました')