Biblioteka Pythona

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

Dostęp do zrenderowanej postaci stron internetowych daje możliwość pobierania danych normalnie niedostępnych (wstawianych np. za pomocą żądań AJAX, JavaScriptowymi wklejkami itd.). Seria artykułów opisuje jak wykorzystać QtWebKit do stworzenia aplikacji zbierającej dane o reklamach wyświetlanych na wskazanych serwisach.

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: Drugi etap to napisanie aplikacji przerabiającej pobrane dane przez "zbieracza": 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: Zaczynamy od zaprojektowania prostego interfejsu aplikacji przedstawionego poniżej:
add1.png
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.png

Kod źródłowy