helper.crawling のソースコード

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
クローリング
    * 指定サイト(site_url)をセレクタ(site_selectors)でクローリングする(ChromeDriverを使用)
    * クローリング結果を、crawling_file_pathに保存する
    * crawling_file_pathのpage_urlsは、スクレイピングする対象urlリストである
    * crawling_file_pathのexclusion_urlsは、スクレイピングの除外urlリストである
    * zip保存まで終わると、page_urlsからexclusion_urlsにurlを移す
    * セレクタを、image_urlで定義すると、crawling_url_deploymentでスクレイピングして末尾画像URLの展開URLでダウンロードして、zipに保存する
    * セレクタを、image_urlsで定義すると、crawling_urlsでスクレイピングしてダウンロードして、zipに保存する
"""
import os
import sys
import copy
import inspect
import json
import datetime
from dataclasses import dataclass
from typing import Union, Optional
import helper.chromeDriver
import helper.webFile
import helper.webFileList
import helper.line_message_api
import helper.slack_message_api
import helper.status


[ドキュメント] @dataclass(frozen=True) class CrawlingValue: """Crawlingの値オブジェクトクラス Attributes: site_url (str): サイトURL site_selectors (Dict[str, str]): サイトセレクタ。キーはアイテム名、値はCSSセレクタ crawling_items (Dict[str, List[str]]): クローリングアイテム。キーはアイテムの種類(例:'page_urls')、値はURLのリスト crawling_file_path (str): クローリングファイルパス Raises: ValueError: 各属性が不正な値(None、空文字列、不正な型)の場合 """ site_url: str = None site_selectors: dict = None crawling_items: dict = None crawling_file_path: str = None def __init__(self, site_url, site_selectors, crawling_items, crawling_file_path): """完全コンストラクタパターン""" if not site_url: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:site_url=None") if not site_selectors: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:site_selectors=None") if crawling_items is None: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:crawling_items=None") if not crawling_file_path: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:crawling_file_path=None") if not isinstance(site_url, str): raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:site_urlがstrではない") if not isinstance(site_selectors, dict): raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:site_selectorsがdictではない") if not isinstance(crawling_items, dict): raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:crawling_itemsがdictではない") if not isinstance(crawling_file_path, str): raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:crawling_file_pathがstrではない") object.__setattr__(self, "site_url", site_url) object.__setattr__(self, "site_selectors", site_selectors) object.__setattr__(self, "crawling_items", crawling_items) object.__setattr__(self, "crawling_file_path", crawling_file_path)
[ドキュメント] class Crawling: """クローリングクラス 指定されたサイトをクローリングし、結果をファイルに保存します """ URLS_TARGET = "page_urls" URLS_EXCLUSION = "exclusion_urls" URLS_FAILURE = "failure_urls" value_object: CrawlingValue = None site_selectors: dict = None crawling_items: dict = {URLS_TARGET: [], URLS_EXCLUSION: [], URLS_FAILURE: []} crawling_file_path: str = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../crawling_list.txt').replace(os.sep, '/') def __init__(self, value_object=None, site_selectors=None, crawling_items=None, crawling_file_path=crawling_file_path): if value_object: if isinstance(value_object, CrawlingValue): value_object = copy.deepcopy(value_object) self.value_object = value_object self.load_text() self.save_text() elif isinstance(value_object, str): if site_selectors: site_selectors = copy.deepcopy(site_selectors) site_url = value_object if crawling_items is None: crawling_items = Crawling.crawling_items self.value_object = CrawlingValue(site_url, site_selectors, crawling_items, crawling_file_path) self.load_text() self.save_text() else: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:site_selectors=None") elif isinstance(value_object, dict): site_selectors = copy.deepcopy(value_object) self.load_text(site_selectors, crawling_file_path) self.save_text() else: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:value_objectが無効な型") else: raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"引数エラー:value_object=None")
[ドキュメント] @staticmethod def scraping(url, selectors): """ChromeDriverを使ってスクレイピングする""" selectors = copy.deepcopy(selectors) chrome_driver = helper.chromeDriver.ChromeDriver(url, selectors) return chrome_driver.get_items()
[ドキュメント] @staticmethod def dict_merge(dict1, dict2): """dict2をdict1にマージする。dictは値がlistであること。list内の重複は削除。list内の順序を維持""" dict1 = copy.deepcopy(dict1) dict2 = copy.deepcopy(dict2) for key, value in dict2.items(): if key in dict1: dict1[key].extend(value) dict1[key] = list(dict.fromkeys(dict1[key])) else: dict1[key] = value return dict1
[ドキュメント] @staticmethod def take_out(items, item_name): """crawling_itemsから指定のitemを取り出す""" ret_value = None if item_name in items: ret_value = copy.deepcopy(items[item_name]) if ret_value and isinstance(ret_value, list): if len(ret_value) == 1: # listの中身が一つしかない時 ret_value = ret_value[0] return ret_value
[ドキュメント] @staticmethod def validate_title(items: dict, title: str, title_sub: str): title = Crawling.take_out(items, title) title_sub = Crawling.take_out(items, title_sub) if not title: if not title_sub: # タイトルが得られない時は、タイトルを日時文字列にする now = datetime.datetime.now() title = f'{now:%Y%m%d_%H%M%S}' else: title = title_sub return helper.chromeDriver.ChromeDriver.fixed_file_name(title)
[ドキュメント] @staticmethod def download_chrome_driver(web_file_list): """selenium chromeDriverを用いて、画像をデフォルトダウンロードフォルダにダウンロードして、指定のフォルダに移動する """ chromedriver = helper.chromeDriver.ChromeDriver() for url, path in zip(web_file_list.get_url_list(), web_file_list.get_path_list()): chromedriver.download_image(url, path)
[ドキュメント] def get_value_object(self): """値オブジェクトを取得する""" if self.value_object: return copy.deepcopy(self.value_object) raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"オブジェクトエラー:value_object")
[ドキュメント] def get_site_url(self): """値オブジェクトのプロパティsite_url取得""" if self.get_value_object().site_url: return copy.deepcopy(self.get_value_object().site_url) raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"オブジェクトエラー:site_url")
[ドキュメント] def get_site_selectors(self): """値オブジェクトのプロパティsite_selectors取得""" if self.get_value_object().site_selectors: return copy.deepcopy(self.get_value_object().site_selectors) raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"オブジェクトエラー:site_selectors")
[ドキュメント] def get_crawling_items(self): """値オブジェクトのプロパティcrawling_items取得""" if self.get_value_object().crawling_items: return copy.deepcopy(self.get_value_object().crawling_items) raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"オブジェクトエラー:crawling_items")
[ドキュメント] def get_crawling_file_path(self): """値オブジェクトのプロパティcrawling_file_path取得""" if self.get_value_object().crawling_file_path: return copy.deepcopy(self.get_value_object().crawling_file_path) raise ValueError(f"{self.__class__.__name__}.{inspect.stack()[1].function}" f"オブジェクトエラー:crawling_file_path")
[ドキュメント] def create_save_text(self): """保存用文字列の作成 以下を保存する * サイトURL * セレクタ * saveファイルのフルパス * クローリング結果urls Returns: str: 保存用文字列の作成 """ __buff = json.dumps(self.get_site_url(), ensure_ascii=False) + '\n' # サイトURL # TODO: selectorsはjson.dumpsでシリアライズできないオブジェクトたぶんlambdaを含んでいる。pickleでもだめらしい。 # 代替方法dillとか https://github.com/uqfoundation/dill # marshalとか検討する # __buff += json.dumps(self.value_object.site_selectors, ensure_ascii=False) + '\n' # セレクタ if self.get_value_object().crawling_file_path: __buff += json.dumps(self.get_crawling_file_path(), ensure_ascii=False) + '\n' # クローリング結果保存パス else: __buff += '\n' # クローリング結果パス追加 __buff += json.dumps(self.get_crawling_items(), ensure_ascii=False) + '\n' # クローリング結果 return __buff
[ドキュメント] def save_text(self): """クローリング情報をファイルに、保存する Returns: bool: 成功/失敗=True/False """ with open(self.get_crawling_file_path(), 'w', encoding='utf-8') as __work_file: __buff = self.create_save_text() __work_file.write(__buff) return True
[ドキュメント] def load_text(self, selectors=None, crawling_file_path=crawling_file_path): """独自フォーマットなファイルからデータを読み込み、value_objectを作り直す Args: selectors (dict, optional): スクレイピングする際のセレクタリスト。デフォルトは None crawling_file_path (str, optional): ダウンロードフォルダのパス。デフォルトは crawling_file_path Returns: bool: 成功、True。ファイルがなかったり、ファイルが空だったら、False """ if not os.path.exists(crawling_file_path): return False if os.stat(crawling_file_path).st_size == 0: return False try: with open(crawling_file_path, 'r', encoding='utf-8') as __work_file: __buff = __work_file.readlines() __site_url = json.loads(__buff[0].rstrip('\n')) # TODO: site_selectors # del __buff[0] # __selectors = json.loads(__buff[0].rstrip('\n')) del __buff[0] __crawling_file_path = json.loads(__buff[0].rstrip('\n')) del __buff[0] __crawling_items = json.loads(__buff[0].rstrip('\n')) del __buff[0] __selectors = selectors __crawling_items2 = None if self.value_object: __site_url = self.get_site_url() __selectors = self.get_site_selectors() __crawling_items2 = self.get_crawling_items() if __crawling_items2: __crawling_items = self.dict_merge(__crawling_items, __crawling_items2) __crawling_file_path = crawling_file_path self.value_object = CrawlingValue(__site_url, __selectors, __crawling_items, __crawling_file_path) except Exception as e: now = datetime.datetime.now().strftime('%Y%m%d%H%M') file_name, ext = os.path.splitext(crawling_file_path) backup_file_path = f"{file_name}_{now}{ext}" os.rename(crawling_file_path, backup_file_path) print(f"ファイルの読み込みに失敗しました。バックアップを作成しました: {backup_file_path}") print(f"エラー内容: {e}") return False return True
[ドキュメント] def is_url_included_exclusion_list(self, url): """除外リストに含まれるURLならTrueを返す """ crawling_items = self.get_crawling_items() if self.URLS_EXCLUSION in crawling_items: if url in crawling_items[self.URLS_EXCLUSION]: return True return False
[ドキュメント] def move_url_from_page_urls_to_exclusion_urls(self, url): """ターゲットリスト(page_urls)から除外リスト(exclusion_urls)にURLを移動する """ site_url = self.get_site_url() selectors = self.get_site_selectors() crawling_file_path = self.get_crawling_file_path() crawling_items = self.get_crawling_items() if self.URLS_EXCLUSION in crawling_items: if url not in crawling_items[self.URLS_EXCLUSION]: crawling_items[self.URLS_EXCLUSION].append(url) else: crawling_items[self.URLS_EXCLUSION] = [url] if self.URLS_TARGET in crawling_items: if url in crawling_items[self.URLS_TARGET]: crawling_items[self.URLS_TARGET].remove(url) self.value_object = CrawlingValue(site_url, selectors, crawling_items, crawling_file_path) self.save_text()
[ドキュメント] def is_url_included_failure_list(self, url): """失敗リストに含まれるURLならTrueを返す """ crawling_items = self.get_crawling_items() if self.URLS_FAILURE in crawling_items: if url in crawling_items[self.URLS_FAILURE]: return True return False
[ドキュメント] def move_url_from_page_urls_to_failure_urls(self, url): """ターゲットリスト(page_urls)から失敗リスト(failure_urls)にURLを移動する """ site_url = self.get_site_url() selectors = self.get_site_selectors() crawling_file_path = self.get_crawling_file_path() crawling_items = self.get_crawling_items() if self.URLS_FAILURE in crawling_items: if url not in crawling_items[self.URLS_FAILURE]: crawling_items[self.URLS_FAILURE].append(url) else: crawling_items[self.URLS_FAILURE] = [url] if self.URLS_TARGET in crawling_items: if url in crawling_items[self.URLS_TARGET]: crawling_items[self.URLS_TARGET].remove(url) self.value_object = CrawlingValue(site_url, selectors, crawling_items, crawling_file_path) self.save_text()
[ドキュメント] def marge_crawling_items(self): """crawling_itemsのpage_urlsにexclusion_urlsがあったら削除する""" crawling_items = self.get_crawling_items() page_urls = [] if self.URLS_TARGET in crawling_items: page_urls = crawling_items[self.URLS_TARGET] for page_url in page_urls: print(page_url) if self.is_url_included_exclusion_list(page_url): self.move_url_from_page_urls_to_exclusion_urls(page_url) continue if self.is_url_included_failure_list(page_url): self.move_url_from_page_urls_to_failure_urls(page_url) continue
[ドキュメント] def crawling_url_deployment(self, page_selectors, image_selectors, notification_id=""): """各ページをスクレイピングして、末尾画像のナンバーから、URLを予測して、画像ファイルをダウンロード&圧縮する # crawling_itemsに、page_urlsがあり、各page_urlをpage_selectorsでスクレイピングする # タイトルとURLでダウンロード除外または済みかをチェックして、 # ダウンロードしない場合は、以降の処理をスキップする # 各page_urlをimage_selectorsでスクレイピングしてダウンロードする画像URLリストを作る。 # 画像URLリストをirvineHelperでダウンロードして、zipファイルにする """ crawling_items = self.get_crawling_items() page_urls = [] if self.URLS_TARGET in crawling_items: page_urls = crawling_items[self.URLS_TARGET] total_pages = len(page_urls) for i, page_url in enumerate(page_urls): status = helper.status.Status() if status is not None and not status.is_running(): print("stop status") break current_page = i + 1 remaining_pages = total_pages - current_page print(page_url) if self.is_url_included_exclusion_list(page_url): self.move_url_from_page_urls_to_exclusion_urls(page_url) continue if self.is_url_included_failure_list(page_url): self.move_url_from_page_urls_to_failure_urls(page_url) continue items = self.scraping(page_url, page_selectors) languages = self.take_out(items, 'languages') title = Crawling.validate_title(items, 'title_jp', 'title_en') url_title = helper.chromeDriver.ChromeDriver.fixed_file_name(page_url) # フォルダがなかったらフォルダを作る os.makedirs(helper.webFileList.WebFileList.work_path, exist_ok=True) target_file_name = os.path.join(helper.webFileList.WebFileList.work_path, f'{title}{url_title}.html') print(title, languages) if languages and languages == 'japanese' and not os.path.exists(target_file_name): # ダウンロードするときだけ通知する # _line_message_api = LineMessageAPI(access_token="", channel_secret="") # _line_message_api.send_message( # notification_id, # f'crawling :現在{current_page}ページ目 / 全{total_pages}ページ中 (残り{remaining_pages}ページ)') _slack_message_api = helper.slack_message_api.SlackMessageAPI(access_token="") _slack_message_api.send_message( notification_id, f'crawling :現在{current_page}ページ目 / 全{total_pages}ページ中 (残り{remaining_pages}ページ)') image_items = self.scraping(page_url, image_selectors) image_urls = self.take_out(image_items, 'image_urls') last_image_url = self.take_out(image_items, 'image_url') if not last_image_url: raise ValueError(f"エラー:last_image_urlが不正[{last_image_url}]") print(last_image_url, image_urls) web_file_list = helper.webFileList.WebFileList([last_image_url]) # 末尾画像のナンバーから全ての画像URLを推測して展開する web_file_list.update_value_object_by_deployment_url_list() url_list = web_file_list.get_url_list() print(url_list) web_file_list.download_irvine() for count in enumerate(helper.webFile.WebFile.ext_list): if web_file_list.is_exist(): break # ダウンロードに失敗しているときは、失敗しているファイルの拡張子を変えてダウンロードしなおす web_file_list.rename_url_ext_shift() web_file_list.download_irvine() if not web_file_list.make_zip_file(): web_file_list.delete_local_files() self.move_url_from_page_urls_to_failure_urls(page_url) continue if not web_file_list.rename_zip_file(title): if not web_file_list.rename_zip_file(f'{title}{url_title}'): sys.exit() web_file_list.delete_local_files() # 成功したらチェック用ファイルを残す helper.chromeDriver.ChromeDriver().save_source(target_file_name) # page_urlsからexclusion_urlsにURLを移して保存する self.move_url_from_page_urls_to_exclusion_urls(page_url) else: # page_urlsからexclusion_urlsにURLを移して保存する self.move_url_from_page_urls_to_exclusion_urls(page_url)
[ドキュメント] def crawling_urls(self, page_selectors, image_selectors): """各ページをスクレイピングして、画像ファイルをダウンロード&圧縮する # crawling_itemsに、page_urlsがあり、各page_urlをpage_selectorsでスクレイピングする # タイトルとURLでダウンロード除外または済みかをチェックして、 # ダウンロードしない場合は、以降の処理をスキップする # 各page_urlをimage_selectorsでスクレイピングしてダウンロードする画像URLリストを作る。 # 画像URLリストをirvineHelperでダウンロードして、zipファイルにする """ crawling_items = self.get_crawling_items() page_urls = [] if self.URLS_TARGET in crawling_items: page_urls = crawling_items[self.URLS_TARGET] for page_url in page_urls: status = helper.status.Status() if status is not None and not status.is_running(): print("stop status") break print(page_url) if self.is_url_included_exclusion_list(page_url): self.move_url_from_page_urls_to_exclusion_urls(page_url) continue if self.is_url_included_failure_list(page_url): self.move_url_from_page_urls_to_failure_urls(page_url) continue items = self.scraping(page_url, page_selectors) title = Crawling.validate_title(items, 'title_jp', 'title_en') url_title = helper.chromeDriver.ChromeDriver.fixed_file_name(page_url) # フォルダがなかったらフォルダを作る os.makedirs(helper.webFileList.WebFileList.work_path, exist_ok=True) target_file_name = os.path.join(helper.webFileList.WebFileList.work_path, f'{title}{url_title}.html') print(title) if not os.path.exists(target_file_name): image_items = self.scraping(page_url, image_selectors) image_urls = self.take_out(image_items, 'image_urls') print(image_urls) web_file_list = helper.webFileList.WebFileList(image_urls) web_file_list.download_irvine() for count in enumerate(helper.webFile.WebFile.ext_list): if web_file_list.is_exist(): break # ダウンロードに失敗しているときは、失敗しているファイルの拡張子を変えてダウンロードしなおす web_file_list.rename_url_ext_shift() web_file_list.download_irvine() if not web_file_list.make_zip_file(): sys.exit() if not web_file_list.rename_zip_file(title): if not web_file_list.rename_zip_file(f'{title}{url_title}'): sys.exit() web_file_list.delete_local_files() # 成功したらチェック用ファイルを残す helper.chromeDriver.ChromeDriver().save_source(target_file_name) # page_urlsからexclusion_urlsにURLを移して保存する self.move_url_from_page_urls_to_exclusion_urls(page_url)