Не могу дважды очистить данные с помощью BeautifulSoup - PullRequest
0 голосов
/ 27 ноября 2018

Я создал скребок для очистки данных с сайта https://www.lawinsider.com.С этого веб-сайта мне удалось получить категорию данных для одной итерации с помощью QThread в отдельном потоке, не блокируя текущий экран графического интерфейса.

В этой предварительной проверке я очищаю контракты с помощью BeautifulSoup на основе выбранной категории, которая являетсянумерация страниц разбита на страницы, поэтому мне приходится разбирать оставшиеся контракты, чтобы звонить отдельно с выбранной категорией.Я преуспел в этой части также.После этого, когда я пытаюсь получить все ссылки на контракты, я получаю None ошибку типа и can not parse HTML для списков контрактов.

Вот мой код:

from bs4 import BeautifulSoup

from PyQt4 import QtGui
from PyQt4.QtCore import QThread, SIGNAL

from fake_useragent import UserAgent

from urllib import request, parse
import sys

from PyDB import LawInsiderDB
import time

# threading tool
# CONSTANTS
HOST = "https://www.lawinsider.com"
CONTRACTS = "{host}/contracts/tagged".format(host=HOST)
SLEEP = 3

Этомой раздел потока, в котором я передаю списки контрактов в формате массива (списка), который выглядит следующим образом:

self.contracts = ["/contracts/hV6Qu7IcxJIXGMj6dkdaw/healthcare-acquisition/1326190/2018-11-27", "/contracts/1TwRbPflPyk61lMcUnraDP/national-commerce-corp/1609951/2018-11-26"]

после прохождения списков контрактов мой код формируется contract_url с использованием глобальногообъект ХОСТ .и извлекать данные с предоставленного URL с помощью плагина urllib с использованием динамического генератора пользовательских агентов, чтобы мой скреппер не блокировался целевым веб-сайтом:

# Thread Calling
class GetContractsThread(QThread):
    def __init__(self, contracts):
        QThread.__init__(self)
        self.contracts = contracts
        self.contract_html = None
        self.db = LawInsiderDB()

    def __del__(self):
        self.wait()

    def get_contract_html(self, url=None):
        # Contract HOST URL to call
        contract_url = HOST + url
        full_url = "{0}".format(contract_url)
        # user agent implementation
        try:
            user_agent = UserAgent(use_cache_server=False)
            request_url = request.Request(full_url, headers={'User-Agent': user_agent.random})
            html = request.urlopen(request_url)
            # html_text = html.text
            self.contract_html = BeautifulSoup(html, 'html.parser')
        except:
            self.contract_html = ''

После анализа HTML-кода по предоставленной ссылке,Я извлекаю заголовок и основное содержимое из проанализированного HTML и сохраняю в базу данных для будущего использования

        # Prase
        try:
            title = self.contract_html.head.title.string
            content = self.contract_html.find("div", class_="contract-content")
        except:
            title = ''
            content = ''

        # need to save into database
        formated_content = str(content.text).replace("'", "")

        # data = str({'title': title, 'url': url})
        # save data
        contracts = "INSERT INTO contracts(cat_id, document, title) " \
                    "VALUES({category_id}, '{document}', '{title}')"\
                    .format(category_id=self.contracts['category_id'], document=formated_content, title=title)
        # end
        if self.db.save_data(contracts):
            # save contract link into database
            save_link = "INSERT INTO contracts_link_history(`category_id`, `contract_id`, `contract_link`) " \
                        "VALUES({category_id}, 0, '{contract_link}')" \
                        .format(category_id=self.contracts['category_id'], contract_link=url)

            if self.db.save_data(save_link):
                return "Saved Contract Link : {url}".format(url=url)
            return "Not Saved Contract Link: {url}".format(url=url)
        else:
            return "Not Saved: {url}".format(url=url)

Ниже код будет запускать мою ветку до тех пор, пока список контрактов не будет завершен:

    def run(self):
        for contract in self.contracts['contracts']:
            contract_html = self.get_contract_html(contract)
            self.emit(SIGNAL('add_completed_contract(QString)'), contract_html)
            self.sleep(SLEEP)
# end

Конец секции потока

Ниже код вызывается, когда я начинаю очистку, и этот код отвечает за создание и вызов QThread, созданного в предыдущем разделе.Позвольте мне объяснить мой метод кода методом

1) Объявление класса

# Scraping Class
class CategoryScrap:

В init Метод: Строка 1: self.db база данныхэкземпляр для выполнения запросов

Строка 2: self.ui Объект MainWindow для взаимодействия с основным пользовательским интерфейсом

Строка 3: self.catVars для хранения всехудаление связанных данных в один объект

Строка 4: self.html для хранения удаленного html

Строка 5: self.start_thread для хранения класса потокаobject

    def __init__(self, ui=None):
        self.db = LawInsiderDB()
        self.ui = ui
        # class objects
        self.catVars = {"host": HOST, "contracts_url": CONTRACTS}
        self.html = None
        self.start_thread = None

Этот метод используется для установки URL-адреса в self.catVars ['url'] объект при передаче URL-адреса веб-сайта в первый раз.URL-адрес имеет вид

https://www.lawinsider.com/contracts/tagged/employment-agreement

    def setUrl(self, url=None):
        if url is not None:
            self.catVars['url'] = url
            # check category is new
            self.check_category()
        else:
            QtGui.QMessageBox.warning(None, "Invalid", "Invalid Domain URL")

Я также управляю своим удаленным содержимым в форме категории, чтобы я мог определить, какое списанное содержимое принадлежит какой категории, и мой метод this проверяет, является ли категорияуже существует или необходимо создать новый:

  • Case 1

Если категория новая, удалите URL-адрес категории и сохраните курсоры и новую категорию, вызвав Метод self.save_new_cursor () & self.create_category () .Здесь курсоры представляют собой точек разбивки на страницы , которые сохраняются в self.catVars следующим образом:

self.catVars['current_cursor'] = "#####"
self.catVars['next_cursor'] = "#####"

В указанной выше точке курсора next_cursor используется дляизвлекать контракты на следующей странице, передавая URL-адрес данной категории в начальном обсуждении.

  • Кейс 2

Если категория уже доступна, тогда курсоры баллов будутбыть последними сохраненными курсорами в базе данных данной категории URL

    def check_category(self):
        # get category name
        category = self.catVars['url'].split('/')[-1]
        self.ui.processingBox.append("Checking category : {title}".format(title=category))
        sql = "SELECT id,name FROM category WHERE `name` LIKE '%{title}%'".format(title=category)
        is_category = self.db.get_single_data(sql)

        # checking category
        if is_category is None:
            new_category_msg = "This is new category. Do you want to create ?"
            qm = QtGui.QMessageBox
            response = qm.question(None, 'New', new_category_msg, qm.Yes | qm.No)
            # if user agreed to create new category
            if response == qm.Yes:
                if self.create_category(category):
                    # get new cursor if categry is new
                    self.scrap_url()
                    self.get_new_cursor()
                    return True
            else:
                self.ui.processingBox.append("Scraping stopped")
                return False
        else:
            self.ui.processingBox.append("Category '{title}' already exists.".format(title=category))
            # if category exists get category information
            self.catVars['category_id'] = is_category['id']
            self.catVars['category_name'] = is_category['name']
            # get existing cursor
            # print(is_category)
            last_cursor_sql = "SELECT id, category_id, current_cursor, next_cursor FROM contract_category_history " \
                              "WHERE `category_id`={category_id} " \
                              "ORDER BY `id` DESC LIMIT 1".format(category_id=self.catVars['category_id'])
            # print(self.db.get_single_data(last_cursor_sql))
            try:
                last_cursor_data = self.db.get_single_data(last_cursor_sql)
                self.catVars['current_cursor'] = last_cursor_data['current_cursor']
                self.catVars['next_cursor'] = last_cursor_data['next_cursor']

                # scrap url if already exists
                if last_cursor_data['current_cursor'] == '':
                    self.scrap_url()
                else:
                    category_url = "{contracts}/{category_name}?cursor={cursor}".format(
                        contracts=CONTRACTS, category_name=is_category['name'],
                        cursor=last_cursor_data['current_cursor'])
                    self.catVars['url'] = category_url
                    self.scrap_url()
            except:
                QtGui.QMessageBox.warning(None, "Error", "Can not get category please check log.")

        # get all contracts from saved and online
        self.get_all_contracts_links()
        # start scraping
        self.start_category_scraping()

Этот метод будет вызываться выше, когда категория новая

    def create_category(self, category):
        new_category = "INSERT INTO `category`(`name`) VALUES('{title}')".format(title=category)
        if self.db.save_data(new_category):
            self.ui.processingBox.append("New category : {title} created.".format(title=category))
            self.catVars['category_id'] = self.db.cursor.lastrowid
            self.catVars['category_name'] = category
            return True
        else:
            QtGui.QMessageBox.warning(None, "Error", "Follow error.log file created in setup directory.")

Этот метод удаляет предоставленный URL и сохраняет в self.html объект, так что при выполнении очистки мой код может быть получен непосредственно из контекста класса.

    def scrap_url(self):
        self.html = None
        user_agent = UserAgent(use_cache_server=False)
        print("Scrap Url: ", self.catVars['url'])
        request_url = request.Request(self.catVars['url'], headers={'User-Agent': user_agent.random})
        try:
            html = request.urlopen(request_url)
            self.html = html
        except:
            print("Error: ", sys.exc_info()[0])
        return True

Этот метод pase очищает HTML и сохраняет новый курсор в базе данных в принадлежащую категорию.

    def get_new_cursor(self):
        try:
            html = self.get_parsed_html(self.html)
            # fetch new cursor
            html_cursors = html.find('div', id="pagination-append")
            data_cursor = [item['data-cursor'] for item in html_cursors.find_all() if 'data-cursor' in item.attrs][0]
            data_next_cursor = [item['data-next-cursor'] for item in html_cursors.find_all() if 'data-next-cursor' in item.attrs][0]
            # store cursor into cursor object
            self.catVars['current_cursor'] = data_cursor
            self.catVars['next_cursor'] = data_next_cursor
            # save new cursor
            if self.save_cursors():
                self.ui.processingBox.append("New cursor point : {title} created.".format(title=self.catVars['category_name']))
        except:
            # if any occurs
            self.ui.processingBox.append("Invalid html to parse")
        return True

Этот метод преобразует проанализированный html в объект Beautifulshop, так что мой код может легко извлечь содержимое из html

    def get_parsed_html(self, html=None):
        if html is not None:
            return BeautifulSoup(self.html, 'html.parser')
        else:
            return BeautifulSoup(html, 'html.parser')

Этот метод сохраняет новые курсоры

    def save_cursors(self):
        save_cursor_sql = "INSERT INTO contract_category_history(category_id, current_cursor, next_cursor) " \
                          "VALUES({}, '{}', '{}')".format(self.catVars['category_id'], self.catVars['current_cursor'],
                                                          self.catVars['next_cursor'])
        try:
            if self.db.save_data(save_cursor_sql):
                return True
            else:
                return False
        except:
            print("Can not save new record", sys.exc_info()[0])

Этот метод удаляет все контракты ссылки и сохраняет в self.catVars ['контракты]] объекты, которые описаны в разделе Thread выше:

    # get all contracts
    def get_all_contracts_links(self):
        try:
            html = self.get_parsed_html(self.html)
            # save contracts link from url
            contract_html = html.find('div', id="pagination-append")
            online_contracts = [a.get('href') for a in contract_html.find_all('a')]
            print(online_contracts)
        except:
            online_contracts = []
            QtGui.QMessageBox.warning(None, "Error", "Html parse error.", sys.exc_info()[0])

        # saved contract list
        saved_contract_sql = "SELECT id, category_id, contract_link FROM `contracts_link_history` " \
                             "WHERE `category_id`={category_id}".format(category_id=self.catVars['category_id'])
        try:
            rows = self.db.get_all_data(saved_contract_sql)
            if rows is not None:
                saved_contract_list = [row['contract_link'] for row in rows]
            else:
                saved_contract_list = []
        except:
            saved_contract_list = []
        # filter online and saved contract list
        self.catVars['contracts'] = list(set(online_contracts)-set(saved_contract_list))
        return True

Этот метод запускает мой поток и передает проанализированные контракты в мой поток когда все контракты заканчиваются, тогда вызывается метод self.scrap_complete () , а когда заканчивается обработка по одному контракту, тогда вызывается метод self.add_completed_contract .

    def start_category_scraping(self):

        if len(self.catVars['contracts']):
            self.start_thread = GetContractsThread(self.catVars)
            self.start_thread.start()
            QtGui.QMainWindow.connect(self.start_thread, SIGNAL("finished()"), self.scrap_complete)
            QtGui.QMainWindow.connect(self.start_thread, SIGNAL("add_completed_contract(QString)"),
                                      self.completed_contract)

            self.ui.stopScrapBtn.setEnabled(True)
            self.ui.stopScrapBtn.clicked.connect(self.start_thread.terminate)
            # We don't want to enable user to start another thread while this one is
            # running so we disable the start button.
            self.ui.startScrapBtn.setEnabled(False)
        else:
            QtGui.QMessageBox.information(None, "Info", "No contract in list.")
        return True

Этоназывается, когда лом заканчивается.После завершения очистки я хочу снова вызвать все методы очистки, чтобы я мог продолжить очистку до завершения пользователем или принудительно остановить его.

Проблема:

Но во второй итерации я не получаю контрактных ссылок , поскольку я получаю в первый раз начатый сборник.Но я могу получить курсор следующего контракта, а затем попытаться получить контракты_ссылки, получая ошибку HTML NoneType

Чтобы получить следующие ссылки контракта, я перехожу по этому виду URL

https://www.lawinsider.com/contracts/tagged/employment-agreement?cursor=ClwKGAoLZmlsaW5nX2RhdGUSCQiAgLT9h9DeAhI8ahVzfmxhd2luc2lkZXJjb250cmFjdHNyIwsSCENvbnRyYWN0IhU0ZUZmaGI5RERjQldCcUFSQTZLUHYMGAAgAQ%3D%3D

В приведенном ниже коде category_url формирует указанную выше ссылку URL:

    def scrap_complete(self):
        print("Scraping Complete")
        time.sleep(SLEEP)
        # get next cursor
        category_url = "{contracts}/{category_name}?cursor={cursor}".format(
            contracts=CONTRACTS, category_name=self.catVars['category_name'],
            cursor=self.catVars['next_cursor'])
        # next url
        self.catVars['url'] = category_url
        self.scrap_url()
        # print(self.html.read())
        self.get_new_cursor()
        # get all contracts from saved and online
        self.get_all_contracts_links()
        # print("New Contracts: ", self.catVars['contracts'])
        # # start scraping
        # self.start_category_scraping()

    def completed_contract(self, contract):
        print("Complete: ", contract)

Если вы нашли мой английский с грамматической ошибкой, учтите это, я постарался уточнить здесь.

...