Я создал скребок для очистки данных с сайта 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 проверяет, является ли категорияуже существует или необходимо создать новый:
Если категория новая, удалите URL-адрес категории и сохраните курсоры и новую категорию, вызвав Метод self.save_new_cursor () & self.create_category () .Здесь курсоры представляют собой точек разбивки на страницы , которые сохраняются в self.catVars следующим образом:
self.catVars['current_cursor'] = "#####"
self.catVars['next_cursor'] = "#####"
В указанной выше точке курсора next_cursor используется дляизвлекать контракты на следующей странице, передавая URL-адрес данной категории в начальном обсуждении.
Если категория уже доступна, тогда курсоры баллов будутбыть последними сохраненными курсорами в базе данных данной категории 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)
Если вы нашли мой английский с грамматической ошибкой, учтите это, я постарался уточнить здесь.