Python - サイトマップ - コンテンツの状態をダンプ

 クラウディア
1. 概要
2. 入力諸元
3. 本体
4. 初期処理
5. チェック
6. コンテンツ解析
7. 出力

1. 概要

 ここでやろうとしているのは、ウェブサーバに何度もアクセスするのは、トラフィックの問題もありますので・・・。  一度、ぐるっとアクセスするときに、サーバ内のコンテンツの状態を収集したら、これを「.json」の形式にダンプしてやろうと思いましてな・・・。  こんな形式になります。

|-- dump.py											本体
`-- Sitemap
    |-- Check.py									チェック
    |-- Common.py									共通処理
    |-- Contents.py									コンテンツ作成
    |-- initial.json								入力諸元
    `-- Initial.py									初期処理

2. 入力諸元

 入力の諸元を、「.json」形式で与えるようにします。  今んとこ(2022年6月16日)

{
	"root"     	: "アクセス先ドキュメントルート",
	"contents" 	: "チェックした結果を出力する「.json」ファイル名",
	"exclude"	: [ リンク先で対象外のものをリスト ],
	"exclkey"	: [ リンク先で対象外となるキーワードのはいったものをリスト ],
	"output"   	: "最終的に出力する sitemap.xml ファイル名"
}

3. 本体

 本体は、こんなの
import json
import ssl
import sys

from Sitemap import Check
from Sitemap import Common
from Sitemap import Contents
from Sitemap import Initial

##
# @file    dump.py
# @version 0.1
# @author  show.kit
# @date    2022年6月16日
# @brief   sitemap を作成するためのデータを収集して、json 形式でダンプする

# 再帰回数の上限を変更

sys.setrecursionlimit(sys.getrecursionlimit()*4)

initial  = Initial.Initial()
common   = Common.Common()
check    = Check.Check(initial)
contents = Contents.Contents()

common.setstart()

# SSL でエラーにならないためのおまじない

ssl._create_default_https_context = ssl._create_unverified_context

print('コンテンツ収集開始 '+common.datetime())

tree      = {}
checklist = []

tree[initial.root] = check.execute(0, checklist, initial.root, contents)

with open(initial.contents, mode='wt', encoding='utf-8') as file:
  json.dump(tree, file, ensure_ascii=False, indent=2)

print('コンテンツ収集終了 '+common.datetime(), '数', len(checklist))
print('処理時間           ', common.getpast())

 「check.execute」を行うまでは、初期処理と言っても問題ない。  部品を集めて、処理開始を「print」ですな。  終了の「print」を行う前に、収集したものを「.json」形式で出力しています。

4. 初期処理

 初期処理は、前々項の入力諸元の「.json」ファイルを読み込むだけです。
import json
import os

filename = './initial.json'

# @file    Initial.py
# @version 0.1
# @author  show.kit
# @date    2020年9月2日
# @brief   ini ファイル取得

class Initial:

###### Initial::__init__
  # @brief  コンストラクタ
  # @return 戻り値なし
  def __init__(self) :

    ## モジュールの位置にディレクトリを移動して
    ## 定義ファイル読み込み

    os.chdir(os.path.dirname(__file__))

    with open(filename, mode='r', encoding='utf-8') as file:
      self.dict = json.load(file)

    self.root     = self.dict['root']                                           # ドキュメントルート
    self.contents = self.dict['contents']                                       # 収集時の出力 .json ファイル名フルパス
    self.exclude  = self.dict['exclude']                                        # 収集対象外 URI のリスト
    self.exclkey  = self.dict['exclkey']                                        # 収集対象外 キーワードリスト
    self.output   = self.dict['output']                                         # 出力 sitemap.xml フルパス
    return

 特に説明もいらないかと思います。

5. チェック

 チェックというか、実際にウェブを徘徊して収集する処理です。
from __future__ import barry_as_FLUFL
import re
import inspect, os

from socket import timeout
from urllib import request
from urllib.error import URLError, HTTPError
from urllib.parse import urlparse
from urllib.parse import urljoin

from bs4    import BeautifulSoup, Comment

from Sitemap import Contents

##
# @file    Check.py
# @version 0.1
# @author  show.kit        更新
# @date    2020年9月3日
# @brief   再帰的にチェックしていく

class Check:

###### Check::__init__
  # @brief  コンストラクタ
  # @return 戻り値なし
  #
  def __init__(self, initial):

    self.headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.108 Safari/537.36' }
    self.init = initial
    self.root = None
    return

###### Check::execute
  # @brief  チェック実行
  # @param  nest
  # @param  uri
  # @param  contents
  # @return 戻り値なし
  #
  def execute(self, nest, checklist, uri, contents):

    if (self.root == None):
      self.root = str(uri)

    if (uri in checklist):
      return

    checklist.append(uri)

    if (not uri.startswith(self.root)):
      return

    for exclude in self.init.exclude:
      if (uri.startswith(exclude)):
        return

    nest += 1

    originalURI = uri

    try:
        requestURL = request.Request(originalURI, headers=self.headers)
        html = request.urlopen(requestURL, timeout=10)
    except (HTTPError, URLError, ConnectionResetError, timeout) as ex:
        print(uri, '  ', ex)
        return None

    except Exception as ex:
        import pdb; pdb.set_trace()

    soup = BeautifulSoup(html, "html.parser")
    html.close()

    contents.get(uri, soup)

    # 下向きにしか行かないので .html はリンクチェックしない

    if (uri.endswith('.html')):
      return contents.contents

    ## リンク先を取得
    #    <a href="...">    抜き出し
    #    tree 構造を取得するだけなので、これだけでよし
    #    つまり onclick は不要

    links = [url.get('href') for url in soup.find_all('a')]

    for tr in soup.find_all('tr'):
      onclick = tr.get('onclick')

      if (type(onclick) is str):

        r = re.findall("'(.*)'", onclick)

        for link in r:

          # tr 内 onlick = "location.href='リンク先'"  のリンク先の文字列を リストする

          links.append(link)

    ## 重複を削除

    links = list(set(links))

    for link in links:

      # 対象外のキーワードが入っているものは チェックしない

      if (type(link) is not str):

        print(uri, 'リンク先', str(type(link)))

        continue

      inflag = False

      for key in self.init.exclkey:
        if key in link:
          try:
            inflag = True
            break
          except Exception as ex:
            import pdb; pdb.set_trace()
            print("[", ex.args, "]")
            exit()

      if inflag:
        continue

      ## ここで再帰呼び出し

      childcontents = Contents.Contents()

      childuri = urljoin(uri, link)

      result = self.execute(nest, checklist, childuri, childcontents)

      if (result == None):
          continue

      contents.contents[childuri] = childcontents.contents

    if ('contents' not in dir(contents)):
      return None

    return contents.contents

 ツリーをたどっていくので、枝の方へのリンクはたどっていきますが、リンクチェックではないため、あえて、上位の構造へのリンクはたどらないようにしています。

6. コンテンツ解析

 前項の処理で収集するごとに、1ページごとにコンテンツを解析してそのオブジェクトを作成する処理です。
from datetime import datetime

import inspect, os
from bs4    import BeautifulSoup, Comment

import json

##
# @file    Contents.py
# @version 0.1
# @author  show.kit        更新
# @date    2020年9月8日
# @brief   コンテンツの生ファイルの更新状況を取得する
#
#          uri, ドキュメントの中身を元に
#          実際のファイルを割り出し
#          コンテンツテーブルのレコードを作成する
#
#          レコードは以下の通り
#             index       インデックス番号(もはや無意味 全部1?)
#             uri         URL
#             document    コンテンツの相対パス+ファイル名
#             title       タイトル
#             modified    更新日時
#             description コンテンツの要約
#             content     コンテンツの内容

class Contents:

###### Contents::__init__
  # @brief  コンストラクタ
  # @return 戻り値なし
  def __init__(self):
    return

###### Contents::get
  # @brief  コンストラクタ
  # @param  uri
  # @param  doc
  # @return 戻り値なし

  def get(self, uri, soup):

    try:
      ## <h1>, <script>, <span> を消去

      for s in soup.select('h1'):
          s.extract()

      for s in soup.select('script'):
          s.extract()

      for s in soup.select('span'):
          s.extract()

      contents = {}

      ## uri head title documnt

      contents['uri']   = uri

      head = soup.find('head')

      contents['title'] = head.find('title').text
      contents['document'] = uri

      ##  Laravel でコンテンツ内に埋め込むようにしたのでそこから取得する        modified

      meta   = head.find('meta', {'name' : 'date'})

      if (meta == None):
        print(uri, '<meta name="date" content=""> なし')
        return None

      update = meta['content']
      contents['modified'] = datetime.strptime(update, '%Y-%m-%dT%H:%M:%S%z').strftime('%Y%m%d%H%M%S')

      ## description 当面はタイトルを適用                                       description

      contents['description'] = contents['title']

      # ここで body から 本文を収集するのであるが
      # 開業しかはいってこないので 未完

      ## 本文                                                                   content
      contents['content'] = soup.find('body').text

      self.contents = contents

    except Exception:
      print(uri, '解析エラー')
      return None

    return

 ここで、ひとつのコンテンツに対して

{
  "自身の URI": {
    "uri": "自身の URI",
    "title": "タイトル名",
    "document": "自身の URI",
    "modified": "コンテンツの更新日時",
    "description": "タイトル名",
    "content": "",
    "リンク先の URI": {
		...
 というオブジェクトを作成していきます、リンク先の「URI」は、自身の先には、並列に作成していき、リンク先は同様の構造をとっていきます。  コンテンツの更新日時は、コンテンツ内に

<meta name="date" content="YYYY-MM-DDTHH:MI:SS+0900">
 の形式で埋め込んでいます。  これをいちいち、手で埋め込むのは労力が大きすぎて、たまりませんので、「PHP」で自動で埋め込むようにしています。  「Laracvel」を使用していて、「.blade.php」の更新日時を取得する方法は、「PHP - Laravel - ビュー(blade)」のあたりをご参照ください。

7. 出力

 出力は、前項で作成したものを「.json」形式のファイルで出力するのと。  実行時に、コマンドラインに下記が出力されます。

コンテンツ収集開始 2022年06月16日 18:04:22

	収集時に何かエラーのあるリンク先等があった場合はその内容を出力

コンテンツ収集終了 2022年06月16日 18:04:46
処理時間            0:00:24.344266
ハイスピードプラン損保との違い世界最大級のオンライン英会話EF English Liveマイニングベース健康サポート特集神戸養蜂場U-NEXT