Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

En 128 líneas de código entra exactamente ESTE browser.

Por su­pues­to, po­dría ha­cer má­s, pe­ro has­ta yo ten­go mis stan­dar­d­s!

  • No usar ;

  • No usar if whate­ve­r: f()

Sal­vo eso, hi­ce al­gu­nos tru­cos su­cio­s, pe­ro, en es­te mo­men­to, es un bro­w­ser bas­tan­te com­ple­to en 127 lí­neas de có­di­go se­gún sloc­coun­t, así que ya ju­gué su­fi­cien­te y ma­ña­na ten­go tra­ba­jo que ha­ce­r.

Pe­ro an­tes, con­si­de­re­mos co­mo se im­ple­men­ta­ron al­gu­nos fea­tu­res (voy a cor­tar las lí­neas pa­ra que la pá­gi­na que­de ra­zo­na­ble­men­te an­gos­ta), y vea­mos tam­bién las ver­sio­nes "nor­ma­le­s" de lo mis­mo. La ver­sión "nor­ma­l" no es­tá pro­ba­da, avi­sen si es­tá ro­ta ;-)

Es­to noes al­go que de­ba apren­der­se. De he­cho es ca­si un tra­ta­do en co­mo no ha­cer las co­sas. Es el có­di­go me­nos pi­tó­ni­co y me­nos cla­ro que vas a ver es­ta se­ma­na.

Es cor­to, es ex­pre­si­vo, pe­ro es feo feo.

Voy a co­men­tar so­bre es­ta ver­sión.

Soporte deProxy

Un bro­w­ser no es gran co­sa si no se pue­de usar con pro­x­y. Por suer­te el sta­ck de red de Qt tie­ne buen so­por­te de pro­x­y. El chis­te es con­fi­gu­rar­lo.

De Vicenzo soporta proxies HTTP y SOCKS parseando la variable de entorno http_proxy y seteando el proxy a nivel aplicación en Qt:

 proxy_url = QtCore.QUrl(os.environ.get('http_proxy', ''))
 QtNetwork.QNetworkProxy.setApplicationProxy(QtNetwork.QNetworkProxy(\
 QtNetwork.QNetworkProxy.HttpProxy if unicode(proxy_url.scheme()).startswith('http')\
 else QtNetwork.QNetworkProxy.Socks5Proxy, proxy_url.host(),\
 proxy_url.port(), proxy_url.userName(), proxy_url.password())) if\
'http_proxy' in os.environ else None

Co­mo es la ver­sión nor­mal de esa co­sa?

if 'http_proxy' in os.environ:
    proxy_url = QtCore.QUrl(os.environ['http_proxy'])
    if unicode(proxy_url.scheme()).starstswith('http'):
        protocol = QtNetwork.QNetworkProxy.HttpProxy
    else:
        protocol = QtNetwork.QNetworkProxy.Socks5Proxy
    QtNetwork.QNetworkProxy.setApplicationProxy(
        QtNetwork.QNetworkProxy(
            protocol,
            proxy_url.host(),
            proxy_url.port(),
            proxy_url.userName(),
            proxy_url.password()))

Los abu­sos prin­ci­pa­les contra py­thon son el uso del ope­ra­dor ter­na­rio pa­ra ha­cer un if de una lí­nea (y ani­dar­lo) y el lar­go de lí­nea.

Cookies Persistentes

Es­to es ne­ce­sa­rio por­que que­rés per­ma­ne­cer lo­guea­do en los si­tios de una se­sión a otra. Pa­ra es­to, pri­me­ro tu­ve que ha­cer un pe­que­ño me­ca­nis­mo de per­sis­ten­cia, y guar­da­r/­leer los cookies de ahí.

Acá está como hice la persistencia (settings is una instancia de QSettings global):

def put(self, key, value):
    "Persist an object somewhere under a given key"
    settings.setValue(key, json.dumps(value))
    settings.sync()

def get(self, key, default=None):
    "Get the object stored under 'key' in persistent storage, or the default value"
    v = settings.value(key)
    return json.loads(unicode(v.toString())) if v.isValid() else default

No es có­di­go muy ra­ro, sal­vo por usar el ope­ra­dor ter­na­rio al fi­na­l. El uso de json me ase­gu­ra que mien­tras me­ta co­sas ra­zo­na­ble­s, voy a ob­te­ner lo mis­mo de vuel­ta, con el mis­mo ti­po, sin ne­ce­si­dad de con­ver­tir­lo o lla­mar mé­to­dos es­pe­cia­le­s.

¿Entonces, como guardo/leo los cookies? Primero se necesita acceder el "cookie jar". No encontré si hay uno global o por view, así que creé un QNetworkCookieJar en la línea 24 y la asigno a cada página en la línea 107.

# Save the cookies, in the window's closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])

# Restore the cookies, in the window's __init__
self.cookies.setAllCookies([QtNetwork.QNetworkCookie.parseCookies(c)[0]\
for c in self.get("cookiejar", [])])

Confieso mi crimen de usar comprensiones de listas cuando la herramienta correcta era un for.

Uso el mis­mo tru­co al res­tau­rar los ta­bs abier­to­s, con el mo­co agre­ga­do de usar una com­pren­sión de lis­ta y des­car­tar el re­sul­ta­do:

# get("tabs") is a list of URLs
[self.addTab(QtCore.QUrl(u)) for u in self.get("tabs", [])]

Propiedades y Señales al crear un objeto

Es­te fea­tu­re es­tá en ver­sio­nes re­cien­tes de Py­Q­t: si pa­sás nom­bres de pro­pie­da­des co­mo ar­gu­men­tos con nom­bre, se les asig­na el va­lo­r. Si pa­sás una se­ñal co­mo ar­gu­men­to con nom­bre, se co­nec­tan al va­lo­r.

Es un fea­tu­re ex­ce­len­te, que te ayu­da a crear có­di­go cla­ro, lo­cal y con­ci­so, y me en­can­ta te­ner­lo. Pe­ro si te que­rés ir a la ban­qui­na, es man­da­da a ha­ce­r.

Es­to es­tá por to­dos la­dos en De Vi­cen­zo, és­te es só­lo un ejem­plo (sí, es una so­la lí­nea):

QtWebKit.QWebView.__init__(self, loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None, loadFinished=self.pbar.hide, loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None, titleChanged=lambda\
t: container.tabs.setTabText(container.tabs.indexOf(self), t) or\
(container.setWindowTitle(t) if self.amCurrent() else None))

Por adon­de em­pie­zo­...

Hay expresiones lambda usadas para definir los callbacks en el lugar en vez de conectarse con una función o método "de verdad".

Hya lamb­das con el ope­ra­dor ter­na­rio:

loadStarted=lambda:\
    self.pbar.show() if self.amCurrent() else None

Hay lambdas que usan or o una tupla para engañar al intérprete y que haga más de una cosa en un solo lambda!

loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None

No voy ni a in­ten­tar des­en­re­dar es­to con fi­nes edu­ca­ti­vo­s, pe­ro di­ga­mos que esa lí­nea con­tie­ne co­sas que de­be­rían ser 3 mé­to­dos se­pa­ra­do­s, y de­be­ría es­tar re­par­ti­da en 6 lí­neas o ma­s.

Download Manager

Lla­mar­lo un ma­na­ger es exa­ge­rar por­que no se pue­de pa­rar una des­car­ga des­pués que em­pie­za, pe­ro bue­no, te de­ja ba­jar co­sas y se­guir bro­w­sean­do, y te da un re­por­te de pro­gre­so!

Primero, en la línea 16 creé un diccionario bars para llevar registro de los downloads.

Des­pué­s, te­nía que de­le­gar el con­te­ni­do no so­por­ta­do al mé­to­do in­di­ca­do, y eso se ha­ce en las lí­neas 108 and 109

Básicamente, con eso cada vez que hacés click en algo que WebKit no puede manejar, se llama al método fetch con el pedido de red como argumento.

def fetch(self, reply):
    destination = QtGui.QFileDialog.getSaveFileName(self, \
        "Save File", os.path.expanduser(os.path.join('~',\
            unicode(reply.url().path()).split('/')[-1])))
    if destination:
        bar = QtGui.QProgressBar(format='%p% - ' +
            os.path.basename(unicode(destination)))
        self.statusBar().addPermanentWidget(bar)
        reply.downloadProgress.connect(self.progress)
        reply.finished.connect(self.finished)
        self.bars[unicode(reply.url().toString())] = [bar, reply,\
            unicode(destination)]

No hay mu­cho golf acá sal­vo las lí­neas lar­ga­s, pe­ro una vez que me­tés en­ters es la ma­ne­ra ob­via de ha­cer­lo:

  • Pe­­dí un no­m­­bre de ar­­chi­­vo

  • Creás un pro­­­gress­­ba­­r, lo po­­­nés en el sta­­tus­­ba­­r, y lo co­­­ne­c­­tas a las se­­ña­­les de pro­­­gre­­so de la des­­ca­r­­ga.

Entonces, por supuesto, está el slot progress que actualiza la barra:

progress = lambda self, received, total:\
    self.bars[unicode(self.sender().url().toString())][0]\
    .setValue(100. * received / total)

Sí, de­fi­ní un mé­to­do co­mo lamb­da pa­ra aho­rrar una lí­nea. [fa­ce­pal­m]

Y elslot finished para cuando termina el download:

def finished(self):
    reply = self.sender()
    url = unicode(reply.url().toString())
    bar, _, fname = self.bars[url]
    redirURL = unicode(reply.attribute(QtNetwork.QNetworkRequest.\
        RedirectionTargetAttribute).toString())
    del self.bars[url]
    bar.deleteLater()
    if redirURL and redirURL != url:
        return self.fetch(redirURL, fname)
    with open(fname, 'wb') as f:
        f.write(str(reply.readAll()))

has­ta so­por­ta re­di­rec­cio­nes co­rrec­ta­men­te! Más allá d eso, na­da más es­con­de la ba­rra, guar­da los da­to­s, fin del cuen­ti­to. La lí­nea lar­ga ni si­quie­ra es mi cul­pa!

Hay un pro­ble­ma en que el ar­chi­vo en­te­ro se man­tie­ne en me­mo­ria has­ta el fin de la des­car­ga. Si te ba­jás un DV­D, te va a do­le­r.

Usar el with ahorra una línea y no pierde un file handle, comparado con las alternativas.

Impresión

De nue­vo Qt me sal­va las pa­pa­s, por­que ha­cer es­to a ma­no de­be ser di­fí­ci­l. Sin em­bar­go, re­sul­ta que el so­por­te de im­pre­sió­n... es­tá he­cho. Qt, es­pe­cial­men­te usa­do vía Py­Qt es tan com­ple­to!

self.previewer = QtGui.QPrintPreviewDialog(\
    paintRequested=self.print_)
self.do_print = QtGui.QShortcut("Ctrl+p",\
    self, activated=self.previewer.exec_)

No ne­ce­si­té na­da de gol­f. Eso es exac­ta­men­te el có­di­go que se ne­ce­si­ta, y es la ma­ne­ra re­co­men­da­da de en­gan­char "C­tr­l+­p" con la im­pre­sión de la pá­gi­na.

Otros Trucos

No hay otros tru­co­s. To­do lo que que­da es crear wi­dge­ts, co­nec­tar unas co­sas con otra­s, y dis­fru­tar la in­creí­ble ex­pe­rien­ce de pro­gra­mar Py­Q­t, don­de po­dés es­cri­bir un web bro­w­ser en­te­ro (s­al­vo el mo­to­r) en 127 lí­neas de có­di­go.

Rodney Dawes / 2011-03-09 02:23:

Doesn't QtWebKit have built-in proxy support that "just works" if you configure it in the system control panel?

Roberto Alsina / 2011-03-09 02:28:

Good question. If there is, I couldn't find it in the docs.

Mario / 2011-03-09 07:30:

Great examples on different topics, printing, network (with redirect), cookie handling, proxy and of course, the power of Python :)

Dan / 2011-03-09 09:38:

Awesome. Just happend to read through wikipedia's "list of browsers" - This one is missing. I'd love to do all the tests and post it there..

Roberto Alsina / 2011-03-09 10:43:

Sure, go ahead :-)

Shulai / 2011-03-10 01:26:

(Py)Qt is powerful indeed, and you are a master to showcase them. The only thing I really miss in Qt and PyQt is built in Poppler support, I got it working on Windows with MinGW but it was somewhat painful.

Anyway, what's your next code golfing challenge? Your public is eager to know! :-)

Roberto Alsina / 2011-03-10 01:28:

A twitter/identi.ca client apparently!

employment background check / 2011-12-27 23:33:


Well, the write-up is truly the freshest on this laudable topic. 

Joe Borg / 2012-11-26 11:52:

Hi, I think there is a bug (maybe with Qt). If you open the application to a page that plants a cookie, then open a tab, close that tab then try to open another one, you get:

Traceback (most recent call last):
File "./qtwk.py", line 15, in <lambda>
self.tabs.setCornerWidget(QtGui.QToolButton(self, text="New Tab", icon=QtGui.QIcon.fromTheme("document-new"), clicked=lambda: self.addTab().url.setFocus(), shortcut="Ctrl+t"))
File "./qtwk.py", line 77, in addTab
self.tabs.setCurrentIndex(self.tabs.addTab(Tab(url, self), ""))
File "./qtwk.py", line 105, in __init__
self.wb.page().networkAccessManager().setCookieJar(container.cookies)
RuntimeError: wrapped C/C++ object of type QNetworkCookieJar has been deleted
Traceback (most recent call last):
File "./qtwk.py", line 62, in closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])
RuntimeError: wrapped C/C++ object of type QNetworkCookieJar has been deleted

Roberto Alsina / 2012-11-26 13:11:

Looks like a race condition where I am trying to set properties in the already closed tab. Easy-ish to work around, though (just try/except it)

Joe Borg / 2012-11-26 13:40:

Hi Roberto, the try / except works, but then you don't pick up the persistent cookies. I've tried to fix it myself, but not found a way so far.
It looks like we're destroying the self.cookies instance when we close a tab, meaning it can't be picked up again and you loose the cookies saved application wide.

Roberto Alsina / 2012-11-28 00:05:

Makes sense. I admit the cookiejar is not very tested ;-)

Joe Borg / 2012-11-29 14:53:

Yep, I'm not trying to knock holes in it, just cool to get it fully working :)
Also trying to get new window to work with tabs. It's odd how easy some hard things are and how hard a few easy things are with QtWebKit; like new windows, cookies etc.

Joe Borg / 2012-11-26 15:27:

Also noticed that if you open a tab, close it then try and click on a link on the original tab, you get a segmentation fault.


Contents © 2000-2020 Roberto Alsina