見出し画像

python3でサイト内リンクを収集してアクセスを判定する

業務でログイン付きサイトのリンク切れを調査しなければならなかったのですが、いかんせんページ数もリンクも多すぎて手作業じゃままならず、巷の無料調査ツールではログイン認証をクリアできなかったりCSV出力できなかったりで要望にマッチしないのでpythonにやってもらうことにしました。

準備

環境はこちら
OS:Windows10
言語:python3.7
主な使用ライブラリ:requests,beautifulsoup4
必要なライブラリはcondaやpipなどを利用してインストールしてください。

実装

コードの全文がこちら。期限もあり、今回の作業が自動化できればいいと思って作ったのであまり汎用的ではありませんが、ログイン認証付き静的サイトのリンク切れ調査はこれを応用すれば出力できると思います。

import requests
from requests.exceptions import Timeout
import sys
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import datetime
import codecs

#global
h_files = set() #調査済みURLリスト
abc_link = set() #探索対象URLリスト
xyz_link = set() #探索済みURLリスト

base = "http://example.amazonaws.com/"
now = datetime.datetime.now()  # 時間まで
fname = 'test' + now.strftime('%Y%m%d%H%M%S') + '.csv'

# ID&pass
USER = "devhogepiyo"
PASS = "piyopiyo"

#start session
session = requests.session()

#login
login_info = {
   "username": USER,
   "password": PASS,
   "cookie": "true"
}

# ログインURL
url_page = "http://example.amazonaws.com/login.action?destination=%2Fpages%2hoge&permissionViolation=true"
res = session.post(url_page, data=login_info)
if res.raise_for_status() == None:
   xyz_link.add(url_page)
   abc_link.add(url_page)
   h_files.add(url_page)

while True:
   # BeauttifulSoupを使用してHTML整形
   soup = BeautifulSoup(res.text, 'html.parser')
   #get title
   title = soup.find('title')
   title_text = ''
   if title is not None:
       title_text = title.string
       if title_text is not None:
           title_text = str(title_text)
       else:
           title_text = ''
   re_url = str(res.url) if res.url is not None else ''
   print(title_text)
   #write CSV
   """f = open(fname, 'a',  encoding='cp932', errors='ignore')
   f.write(title_text + ',' + str(re_url) + '\n')
   f.close()"""
   # aタグからURLを取得し、HTTPリクエストを送る
   for link in soup.find_all('a', href=True):
       url = link.get('href')
       link_text = link.string
       if url is None:
           continue
       elif url == '#':
           continue
       elif link_text is not None:
           link_text = link_text.replace(',', ' ')
       url = urljoin(base, url)  # 絶対パスに
       #内外判定
       if url.startswith(base) or url.startswith("https://example.amazonaws.com/"):
           link_kind = '内部リンク'
           if '/pages/' in url:  # 内部リンクのpages配下は探索対象
               abc_link.add(url)
       else:
           link_kind = '外部リンク'
       #重複防止
       if url in h_files:
           continue
       else:
           option = ''
           try:
               res = requests.get(url, timeout=(5.0, 7.5))
               st = str(res.status_code)
               if ('docs.google.com' in url) or ('drive.google.com' in url):
                   st = '403'
                   option = 'googleDrive'
                   print('google' + url)
               elif '/cacoo/' in url:
                   st = '403'
                   option = 'cacoo'
               print(st)
           except requests.exceptions.RequestException:
               print('timeout:' + str(res.status_code))
               st = 'timeout'
               pass
           h_files.add(url)

       # 結果をCSVに出力する
       f = codecs.open(fname, 'a', encoding='cp932', errors='ignore')
       f.write(str(link_text) + ',' + st +
               ',' + url + ',' + link_kind + option + '\n')
       f.close()
#next target url
   if abc_link != xyz_link:
       sa = list(abc_link - xyz_link)
       for s in sa:
           if bool(s) == False:
               continue
           else:
               try:
                   res = session.get(s, data=login_info)
                   #res.raise_for_status()
                   break
               except requests.exceptions.RequestException:
                   print('timeout.')
                   pass
               finally:
                   xyz_link.add(s) #探索済set
   else:
       print('Done')
       sys.exit()

上から順に解説していきます。

import

import
import requests
from requests.exceptions import Timeout
import sys
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import datetime
import codecs

頭のimport宣言。こちらでライブラリなど使いたい外部ソースを読み込みます。試行錯誤してるうちに使おうと思ったけど結局使ってない文が残って混迷してきてしまいがちな箇所でもあります。一応記事にするにあたり不要な記述を整理したつもりですが、エラーが発生したら適宜必要なライブラリをインポートしてください。
今回の処理の中心になるのはrequestsとBeautifulsoup4。requestsはその名の通りリクエストの生成やレスポンスの確認に、Beautiulsoup4はHTMLの要素抽出に便利なライブラリです。

パラメータや配列などの準備

#global
h_files = set() #調査済みURLリスト
abc_link = set() #探索対象URLリスト
xyz_link = set() #探索済みURLリスト

#globalではグローバル変数として三つのset型を用意しています。1つ目は同じURLにリンク切れ調査を行わないように調査済みURLを保持するコレクション。2つ目は見つけたリンクからサイト内のURLだけを保持するコレクション。3つ目はリンクを抽出したURLを保持するコレクションです。なおset型は数学でいう集合で、順序を気にせず重複しないデータ群を扱うのに適した型のようです。今回は同じURLを見つけても調査しない方針で考えたのでset型を採用してます。

base = "http://example.amazonaws.com/"
now = datetime.datetime.now()  # 時間まで
fname = 'test' + now.strftime('%Y%m%d%H%M%S') + '.csv'

# ID&pass
USER = "devhogepiyo"
PASS = "piyopiyo"

#start session
session = requests.session()

#login
login_info = {
   "username": USER,
   "password": PASS,
   "cookie": "true"
}

baseに相対リンクを絶対リンクに変える用のサイトドメイン、nowは現在時刻を取得してフォーマット、文字列結合でファイル名を作りfnameに格納してます。
#ID&passには対象サイトのログインIDとパスワードを設定して、login_infoに本来formログインフォームから送られる情報をjson形式で用意します。ログインにどんなデータが必要か、どんな名前で送られているかはブラウザの機能で直接サイトを検証して調べます。requests.session()でログイン付きサイトに必須なセッションを開始します。

ログイン

# ログインURL
url_page = "http://example.amazonaws.com/login.action?destination=%2Fpages%2hoge&permissionViolation=true"
res = session.post(url_page, data=login_info)
if res.raise_for_status() == None:
   xyz_link.add(url_page)
   abc_link.add(url_page)
   h_files.add(url_page)

session.post()でサイトにログインします。第一引数にログインページのURL、第二引数に送信されるデータを指定します。

タイトル

while True:
   # BeauttifulSoupを使用してHTML整形
   soup = BeautifulSoup(res.text, 'html.parser')
   #get title
   title = soup.find('title')
   title_text = ''
   if title is not None:
       title_text = title.string
       if title_text is not None:
           title_text = str(title_text)
       else:
           title_text = ''
   re_url = str(res.url) if res.url is not None else ''
   print(title_text)
   #write CSV
   """f = open(fname, 'a',  encoding='cp932', errors='ignore')
   f.write(title_text + ',' + str(re_url) + '\n')
   f.close()"""

ページ遷移後はBeautifulSoupを使用してHTMLを読みこみます。これでWEBページ内に記述されている情報をpythonで扱うことができます。
その下では<title>タグを取得して内部のテキストをURLと一緒にCSVに書き出しています。

ページ内部の探索

# aタグからURLを取得し、HTTPリクエストを送る
   for link in soup.find_all('a', href=True):
       url = link.get('href')
       link_text = link.string
       if url is None:
           continue
       elif url == '#':
           continue
       elif link_text is not None:
           link_text = link_text.replace(',', ' ')
       url = urljoin(base, url)  # 絶対パスに

ここからページ内のリンクを収集してリクエストを送っていきます。soup.find_allでaタグのhrefがあるものを全て抽出しforでlinkに入れて処理していきます。linkに入っているものはaタグのままなのでhrefに指定されているURLを抜き出すとともにlink.stringでリンクが掛けられているテキスト文を取り出します。下のif文ではurlが未指定のもの、'#'が指定されているものへの処理をSkipする記述です。最後のelifはリンクテキストにカンマが入っているとCSVをExcelで開いたときに列がズレてしまうためカンマをスペースに変換しています。その後urljoinで絶対パスに変換します。

#内外判定
       if url.startswith(base) or url.startswith("https://example.amazonaws.com/"):
           link_kind = '内部リンク'
           if '/pages/' in url:  # 内部リンクのpages配下は探索対象
               abc_link.add(url)
       else:
           link_kind = '外部リンク'
       #重複防止
       if url in h_files:
           continue
       else:
           option = ''
           try:
               res = requests.get(url, timeout=(5.0, 7.5))
               st = str(res.status_code)
               print(st)
           except requests.exceptions.RequestException:
               print('timeout:' + str(res.status_code))
               st = 'timeout'
               pass
           h_files.add(url)

次に内部リンクと外部リンクを選別しています。今回はページ内のリンクを収集しつつ次に探索すべきURLを見つけていく仕様になっているので、ドメインで見つけたURLが内部リンクか外部リンクかを判定します。httpとhttpsが混在していたのでorで両方かかるようにしてます。今回のケースでは探索対象ページは全て/pages/配下にあったので、/pages/が含まれるかも見てます。該当するURLは探索対象用setの中に放り込みます。
それから一度リクエストしたURLにはリクエストしない方針なので確認済setの中にURLが含まれる場合は処理をskipします。その後URLにリクエストを送り、ステータスコードを取得します。このときtimeoutするとエラーになっり止まってしまうので例外処理でキャッチし処理を続行します。

       # 結果をCSVに出力する
       f = codecs.open(fname, 'a', encoding='cp932', errors='ignore')
       f.write(str(link_text) + ',' + st +
               ',' + url + ',' + link_kind + '\n')
       f.close()

そして結果をCSVに出力します。

次の探索対象ページに遷移

#next target url
   if abc_link != xyz_link:
       sa = list(abc_link - xyz_link)
       for s in sa:
           if bool(s) == False:
               continue
           else:
               try:
                   res = session.get(s, data=login_info)
                   #res.raise_for_status()
                   break
               except requests.exceptions.RequestException:
                   print('timeout.')
                   pass
               finally:
                   xyz_link.add(s) #探索済set
   else:
       print('Done')
       sys.exit()

for文によるページ内の全てのリンク調査がおわったら次のページに遷移します。探索対象URLsetの中からまだ探索していないURLを取り出し遷移に成功したらWhileの先頭へループします。timeoutしたら他のURLを試します。試したURLは探索済みURLsetへ入れます。探索対象URLsetと探索済みURLsetが等しくなった時すべての処理を終了します。

補足

今回のスクリプトは静的サイトを対象にしています。たとえばjsでURLが出現するようなサイトには使えません。サイト内のページ全てがリンクでつながっていることを前提に作成しています。
この記事を7割書いた後でページツリーから探索対象を取得する方式に変えたので書く気力があれば次回の記事にします。

参考:
Python Snippets
Pythonでページ遷移を繰り返しながらスクレイピング
【Python】requestsマスター〜リトライ〜通信の例外処理まで
Requestsクイックスタート
PythonでWebスクレイピングする時の知見をまとめておく
Python3でクロールしようと思って調べたこと

この記事が気に入ったらサポートをしてみませんか?