- 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
|