(Ez a bejegyzés egy kicsit módosítva lett 2015. február 9-én)
Rendbe, most hogy ilyen szépen elkészítettük a saját PyV8 példányunkat, ideje lenne kipróbálni valami kóddal, hogy működik-e rendesen.
A saját projektjeim közül hoztam a mostani témát: egy weboldalon egy Javascript Object
értékét szeretném megkapni és átalakítani dict
-é. A mostani történetben a csavar az, hogy ez az objektum egy függvény paramétere, ami ráadásul a jQuery ready()
metódusába van belenyomogatva. Tehát nem holmi pi=3.14
változót kellene kipiszkálni.
(A projektről homályosan: Van egy weboldal, amin egy Google Maps-es térkép látható. Erre a térképre valamilyen saját library-vel markereket tesznek. Én egy adott, sokszög alapú területen levő markereket akarom kigyűjteni. A probléma megoldásának első részével foglalkozik a mai bejegyzés: a weboldal térképén látható összes koordináta összegyűjtésével. Egy későbbi részben majd kitaláljuk, hogy hogyan lehet kiválogatni azokat, amik egy területen belül vannak.)
Lássuk a kódot
Ebben a kis részletben csak a lényeg látszódik. Azzal hogy hogy halásztam ki a DOM-ból most nem foglalkozok (amúgy lxml-el):
$(document).ready(function(){
new AwesomeLibrary.PutMarkers('#map', {
data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
});
});
Maga a forráskód nagyon egyértelmű, a PutMarkers()
második paraméterének az értékére van szükség ({data:[{id: 1, lat: '47.160971', lng: '16.4878386'}]}
)
Megoldás
A jó multkor egyszerűbbnek találtam, ha először egy fake Javascript kódot eval()
-olok a contextben, amivel módosítottam a később ténylegesen meghívott metódusok működését. Ahhoz hogy ez most is működjön kézzel deklarálnom kell a $
, document
(mivel itt nincs DOM), ready()
, AwesomeLibrary
, PutMarkers
értékeit, hogy ne kapjak sehol se undefined is not a function
errort. Így szépen végig fut az egész kóceráj, de a sajnos a PutMarkers
visszatérési értékét az eredeti függvény nem teszi ki globális változónak, és maga az anonim függvény se tér vissza semmivel se (főleg nem a koordinátákkal). Jobb ötlet híján a saját PutMarkers
implementációmban kiraktam globális változóba a kapott paramétereket, de aztán inkább úgy döntöttem, hogy kipróbálok valami mást, és minden hiányzó függvényt egy Python osztályból rakosgatok a contextbe.
class Globals(PyV8.JSClass):
def __init__(self):
super(Globals, self).__init__()
self.coords = []
def PutMarkers(self, selector, coords):
coords = PyV8.convert(coords)
for coord in coords['data']:
self.coords.append({
'lat': float(coord['lat']),
'lng': float(coord['lng']),
'id': int(coord['id'])
})
def ready(self, function):
function()
def __call__(self, *args, **kwargs):
return self
def __getattr__(self, *args):
return self
Az osztály legfontosabb metódusa a __getattr__()
. Ez minden olyan esetben meghívódik, ha a Python nem találja az objektum egy property-jét. Mivel ez a metódus a globális névtérbe került, ezért minden olyan esetbe, amikor a V8 a globális névtérből keres egy változót, ez a Python metódus fog lefutni. Ide kerül minden olyan logika ami azokat az eseteket kezeli le, amikor valami deklarálatlan értéket keres a V8, de egyáltalán nem fontos, hogy mi annak az értéke. Másik fontos metódus a __call__()
. Ennek segítségével a Globals
példányunk függvényekhez hasonlóan tud viselkedni (meg lehet hívni).
A két magic method kombinációjával meg lehet oldani, hogy a számunkra nem érdekes változókat és metódusokat ne kelljen kézzel deklarálgatni. Amikor a V8 egy property-t keres, akkor szépen megkapja a objektumunkat a __getattr__()
-ből. Ha abban a property-ben még egy propertyt keres, akkor megint csak megkapja az objektumot, de ha a proprty értékét callable-nek tekinti az is működni fog, mivel az objektum függvényként is tud viselkedni.
Magyarul tehát a Javascript kódban bármikor is egy deklarálatlan változót talál a V8, akkor azt mindig a Globals
objektum attribútumai között keresi, és bármikor is talál egy deklarálatlan függvényt, meg fogja tudni hívni, mert a Globals
példánya callable. Gyakorlatilag ez egy lánc (method chaining), így mindig minden keresett érték deklarált lesz. (Nem kerülünk rekurzióba, mivel a property-k kibontogatása véges).
Ha ez megvan, akkor jöhetnek az “érdekes metódusok”, szóval azok, amik ténylegesen végeznek valami munkát. A Javascript forráskódban ezek a ready()
és PutMarkers()
.
A
$
mindegy hogy mit csinál, csak függvény legyen, és valami olyan objektummal térjen vissza, amiben létezik aready()
metódus. A__getattr__()
visszatér egy objektummal, ami függvényként viselkedik, és függvény által visszakapott objektumban létezik aready()
metódus (lásd kicsit lejjebb).
Adocument
,AwesomeLibrary
értékei hasonlóan. Egy csomót spóroltunk!
A ready()
metódus a Globals
osztályban magkapja az anonim függvény, egy JSFunction
Python objektumként. Ezt ugyan úgy meg lehet hívni mint bármelyik sima függvényt, de természetesen a kódot a V8 futtatja, a kontextusban.
A történet vége a PutMarkers()
metódus. Ez 2 paramétert vár, egy css selectort, amivel nem csinálunk semmit, és magát az értékes adatot, szóval a koordinátákat. Innen már nagyon egyszerű a dolog, a PyV8.convert()
átalakítja dict
-é az Javascript Object
-et (így kikerül a context-ből az adat), majd egy sima iterációval bepakolgatjuk az összes koordinátát a self.coords
tömbbe.
script_content = """
$(document).ready(function(){
new AwesomeLibrary.PutMarkers('#map', {
data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
});
});"""
context_globals = Globals()
with PyV8.JSContext(context_globals) as ctx:
ctx.eval(script_content)
print(context_globals.coords)
Példányosítjuk a Globals
osztályt, majd nyitunk egy JSContext
-et. Paraméternek átadjuk a csinos objektumunkat. A contextben pedig futtatjuk a Javascript kódot (script_content
).
A kontextuson kívül a context_globals.coords
propertyben már ott is vannak a koordináták
>>> print(context_globals.coords)
[{'lat': 47.160971, 'id': 1, 'lng': 16.4878386}]
Zárás
Azt hiszem jól látszódik, hogy kivállóan együtt tudnak működni a Python-os osztályok és a V8. Lényegesen elegánsabb ez a megoldás, mint kézzel kikapuzni egy másik Javascript fájlban az összes előforduló de haszontalan változót és aztán betölteni azt a context elején.
A teljes kód megtekinthető ezen a Gist-en.
A következő részben kitaláljuk, hogy hogy lehet kiválogatni az egy területen levő koordinátákat.
-slp
Nincsenek megjegyzések:
Megjegyzés küldése