Wykorzystanie WebKit/PyQt4 do zbierania danych, część 1

Udostępnienie kompletnego silnika przeglądarki WebKit w bibliotece Qt i zarazem PyQt4 dało programistom pole do tworzenia nowych aplikacji operujących na stronach internetowych. Przykładowo przeglądając kod źródłowy różnych stron zobaczymy że reklamy (szczególnie flashowe, reklamy z systemów reklamowych) umieszczane są w postaci wklejek JavaScriptowych - nie wiemy jaka reklama zostanie wyświetlona i gdzie będzie kierować. By dostać takie informacje musimy mieć dostęp do zrederowanej strony, gdzie ta wklejka wyświetliła konkretną reklamę. Można osiągnąć to stosująć QtWebKit. Nasza aplikacja rozwijana w szeregu artykułów będzie zbierać dane o wyświetlanych reklamach z zadanych stron internetowych. Biznesowo informacje kto i gdzie emituje reklamy mogą być przydatne (czy też np. kontrola własnych kampanii reklamowych).

Plan

Zaczniemy od aplikacji zbierającej reklamy i zapisującej dane do bazy SQLite. Cele dla aplikacji to:
  • Automatycznie ładuje po kolei strony z podanej listy N razy
  • Po każdym załadowaniu strony wyszukuje w jej kodzie reklam i zapisuje do bazy ClickTag (URL reklamy), źródło itd.
  • Rozpoznawane formaty reklam rozwiązane za pomocą prostych funkcji umożliwiając łatwą rozbudowę możliwości "zbieracza"
Drugi etap to napisanie aplikacji przerabiającej pobrane dane przez "zbieracza":
  • Ładuje adres ClickTag i zapisuje do bazy tytuł i adres strony finalnej
Opcjonalnie możemy poszerzyć ją o prezentację wyników (choć może je generować też prosty skryp Pythona, aplikacja Django itd. - dane są w łatwo dostępnej bazie).
Gotowy pakiet PyQt4 przeznaczony dla MS Windows zawiera sterownik tylko dla SQLite. Jeżeli chcemy wykorzystać inną bazę danych i uruchamiać aplikację pod tym systemem to albo musimy użyć standardowego pythonowego modułu, lub zabawić się w kompilację PyQt4 pod Windowsem (co z pewnością zawstydzi nawet turbodymomena).

Zbieracz

Plan działania aplikacji jest następujący:
  • Klikamy start - aplikacja pobiera pierwszą stronę z listy i ładuje ją
  • Po załadowaniu odświeża ją N-razy
  • Za każdym razem wyciąga ze strony reklamy i zapisuje do bazy (strona, na której była reklama, adres na który reklama kieruje)
  • Kontynuuje pobierając kolejną stronę z listy aż do przerobienia wszystkich stron
Zaczynamy od zaprojektowania prostego interfejsu aplikacji przedstawionego poniżej:
add1
Mamy przycisk "Start" (QPushButton), dwa paski postępu QProgressBar - jeden mierzący postęp w przerobionych stronach z listy, a drugi ilość wykonanych odświeżeń danej strony. Do tego oczywiście QWebView - widżet przeglądarki. Etykiety widżetów widać w menu po prawej stronie na fotce. Generuję następnie klasę Pythona z interfejsu:
pyuic4 gather.ui > gather.py
I mogę przystąpić do stworzenia szkieletu uruchamiającego aplikację run.py:
# -*- coding: utf-8 -*-
import sys

from PyQt4 import QtCore, QtGui
from gather import Ui_gatherer

class GatherAds(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QWidget.__init__(self, parent)
		self.ui = Ui_gatherer()
		self.ui.setupUi(self)
		# ilość odświeżeń strony
		self.refreshSite = 5
		
		# lista stron
		self.sites = [{'url': 'http://auth.gazeta.pl/googleAuth/login.do', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.gazeta.pl/kobieta/0,0.html', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.wp.pl', 'site': 'wp.pl'},
		]
	
if __name__ == "__main__":
	app = QtGui.QApplication(sys.argv)
	myapp = GatherAds()
	myapp.show()
	sys.exit(app.exec_())

Powyższy kod wyświetla okno aplikacji, oraz zawiera przykładową listę stron, z których będą wyciągane reklamy. self.sites jest listą słowników, z których każdy zawiera url - adres URL strony, oraz site - tekstową nazwę głównego serwisu (grupowanie stron z jednego serwisu).

Kolejny etap to implementacja mechanizmu ładującego po kolei strony. Musimy uwzględnić to że kolejną stronę możemy załadować dopiero po wczytaniu poprzedniej. Oto implementacja takiego rozwiązania:
# -*- coding: utf-8 -*-
import sys

from PyQt4 import QtCore, QtGui
from gather import Ui_gatherer

class GatherAds(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QWidget.__init__(self, parent)
		self.ui = Ui_gatherer()
		self.ui.setupUi(self)
		self.refreshSite = 5
		
		self.currentIndex = 0
		self.sites = [{'url': 'http://auth.gazeta.pl/googleAuth/login.do', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.gazeta.pl/kobieta/0,0.html', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.wp.pl', 'site': 'wp.pl'},
		]
		
		QtCore.QObject.connect(self.ui.startButton,QtCore.SIGNAL("clicked()"), self.start)
		QtCore.QObject.connect(self.ui.webView,QtCore.SIGNAL("loadFinished (bool)"), self.loadFinished)
		QtCore.QObject.connect(self.ui.webView,QtCore.SIGNAL("loadProgress (int)"), self.loadProgress)
		
	def start(self):
		"""
		Start loading the web pages
		"""
		self.ui.startButton.setEnabled(False)
		nexturl = self.__getNextUrl()
		if nexturl:
			self.ui.webView.load(nexturl)
	
	def loadFinished(self):
		"""
		A page was loaded - get the data and load next page
		"""
		page = self.ui.webView.page()
		frame = page.currentFrame()
		content = frame.toHtml()
		print u'Page content, got %s bytes' % len(content)
		
		# process the data here
		
		self.currentIndex += 1
		nexturl = self.__getNextUrl()
		if nexturl:
			self.ui.webView.load(nexturl)
	
	def loadProgress(self, progress):
		"""
		Print the progress of page load
		"""
		print progress
	
	def __getNextUrl(self):
		"""
		Return next URL in list
		"""
		if len(self.sites) - 1 >= self.currentIndex:
			newurl = QtCore.QUrl(self.sites[self.currentIndex]['url'])
		else:
			print 'No next url'
			newurl = False
		
		return newurl
		

if __name__ == "__main__":
	app = QtGui.QApplication(sys.argv)
	myapp = GatherAds()
	myapp.show()
	sys.exit(app.exec_())

Najważniejsza jest tutaj metoda __getNextUrl, która zwraca URL podanego elementu listy. self.currentIndex zaczyna od 0 (pierwszy element listy) i po wczytaniu strony w loadFinished jest inkrementowany o 1, po czym znowu wywoływana jest metoda __getNextUrl zwracająca już kolejny URL - i tak aż do końca (gdy metoda zwróci False). Metoda start podpięta pod kliknięcie przycisku "Start" ładuje pierwszą stronę rozpoczynając tym samym cały cykl. Pomocniczo wykorzystałem loadProgress by wyświetlać w konsoli postęp ładowania stron.

Strony już się ładują, lecz można ten proces przyśpieszyć. Nie potrzebujemy pobierać np grafik. Za pomocą QtWebKit.QWebSettings można wyłączyć ich pobieranie - przyśpieszając tym samym ładowanie się stron. Wystarczy do __init__ dodać poniższy kod (i zaimportować QtWebKit):
s = self.ui.webView.settings()
s.setAttribute(QtWebKit.QWebSettings.AutoLoadImages, False)
s.setAttribute(QtWebKit.QWebSettings.JavascriptCanOpenWindows, False)
s.setAttribute(QtWebKit.QWebSettings.PluginsEnabled, False)
Strony już ładują się po kolei, lecz tylko raz. Musimy dodać obsługę kilkukrotnego ładowania tej samej strony i przy okazji podpiąć postęp pod paski postępu. Oto kod:
# -*- coding: utf-8 -*-
import sys

from PyQt4 import QtCore, QtGui, QtWebKit
from gather import Ui_gatherer

class GatherAds(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QWidget.__init__(self, parent)
		self.ui = Ui_gatherer()
		self.ui.setupUi(self)
		self.refreshSite = 3
		
		self.currentIndex = 0
		self.currentRefresh = 0
		self.sites = [{'url': 'http://auth.gazeta.pl/googleAuth/login.do', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.gazeta.pl/kobieta/0,0.html', 'site': 'gazeta.pl'},
		{'url': 'http://kobieta.wp.pl', 'site': 'wp.pl'},
		]
		
		s = self.ui.webView.settings()
		s.setAttribute(QtWebKit.QWebSettings.AutoLoadImages, False)
		s.setAttribute(QtWebKit.QWebSettings.JavascriptCanOpenWindows, False)
		s.setAttribute(QtWebKit.QWebSettings.PluginsEnabled, False)
		
		QtCore.QObject.connect(self.ui.startButton,QtCore.SIGNAL("clicked()"), self.start)
		QtCore.QObject.connect(self.ui.webView,QtCore.SIGNAL("loadFinished (bool)"), self.loadFinished)
		QtCore.QObject.connect(self.ui.webView,QtCore.SIGNAL("loadProgress (int)"), self.loadProgress)
		
	def start(self):
		"""
		Start loading the web pages
		"""
		self.ui.startButton.setEnabled(False)
		nexturl = self.__getNextUrl()
		if nexturl:
			self.ui.webView.load(nexturl)
	
	def loadFinished(self):
		"""
		A page was loaded - get the data and load next page
		"""
		page = self.ui.webView.page()
		frame = page.currentFrame()
		content = frame.toHtml()
		print u'Page content, got %s bytes' % len(content)
		
		# process the data here
		
		if self.currentRefresh < self.refreshSite:
			print 'Refresh +1'
			self.currentRefresh += 1
		else:
			print 'Index +1'
			self.currentRefresh = 0
			self.currentIndex += 1
		
		nexturl = self.__getNextUrl()
		if nexturl:
			self.ui.webView.load(nexturl)
	
	def loadProgress(self, progress):
		"""
		Print the progress of page load
		"""
		print progress
	
	def __getNextUrl(self):
		"""
		Return next URL in list
		"""
		# set the progress bar of pages loaded
		progress_value = (float(self.currentIndex)/float(len(self.sites)))*100
		self.ui.sitesBar.setValue(progress_value)
		
		# set the progress bar of refreshes
		progress_value = (float(self.currentRefresh)/float(self.refreshSite))*100
		self.ui.iterationBar.setValue(progress_value)
		
		if len(self.sites) - 1 >= self.currentIndex:
			newurl = QtCore.QUrl(self.sites[self.currentIndex]['url'])
		else:
			print 'No next url'
			newurl = False
		
		return newurl
		

if __name__ == "__main__":
	app = QtGui.QApplication(sys.argv)
	myapp = GatherAds()
	myapp.show()
	sys.exit(app.exec_())
Podobnie jak przy ładowaniu kolejnych stron używamy pomocniczej zmiennej self.currentRefresh przechowującą obecną krotność odświeżenia danej strony. W loadProgress jeżeli jej wartość jest mniejsza od ilości odświeżeń jaką chcemy osiągnąć (self.refreshSite) to nie zwiększamy indeksu self.currentIndex tylko samo self.currentRefresh - przez co załadowana zostanie ta sama strona. Gdy ilość odświeżeń dojdzie do limitu - zerujemy ilość odświeżeń, zwiększamy indeks - co rozpoczyna ładowanie kolejnej strony. W __getNextUrl dodałem też postęp dla sitesBar i iterationBar licząc odpowiednio ilość przerobionych już stron i ilość przerobionych odświeżeń (w procentach).

Nasza aplikacja jest już kompletna jeżeli chodzi o ładowanie stron. Teraz trzeba napisać logikę odpowiedzialną za wydobywanie danych ze strony i zapis odpowiednich wyników do bazy danych. Tym zajmiemy się w następnym artykule.

add2

Kod źródłowy

RkBlog

PyQt, 31 October 2009

Comment article
Comment article RkBlog main page Search RSS Contact