A következő címkéjű bejegyzések mutatása: pyv8. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: pyv8. Összes bejegyzés megjelenítése

2015. február 8., vasárnap

Javascript függvény paraméterének kiolvasása PyV8-al

(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 a ready() metódus. A __getattr__() visszatér egy objektummal, ami függvényként viselkedik, és függvény által visszakapott objektumban létezik a ready() metódus (lásd kicsit lejjebb).
A document, 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

2015. január 25., vasárnap

PyV8 még egyszer (Python 3 + pyenv)

Ma rendesen megszopattam magam, ugyanis nem kisebb probléma megoldásának álltam neki mint hogy lefordítom Python 3 alá a PyV8-at (Python 2 alá ez már egyszer sikerül). Az extra csavar a történetben az, hogy az interpreterem pyenv környezetben létezik.

A jövőben mindenképp írok még egy teljes bejegyzést a pyenvről. Egyelőre elég annyit tudni róla, hogy ez egy nagyszerű shell script, amivel pofonegyszerűen lehet menedzselni és váltogatni ugyan azon az operációs rendszeren a különböző verziójú Python interpretereket. Akár minden könyvtárban, minden shellben más verziót lehet futtatni, sőt, a virtualenv-ek kezelését is rá lehet bízni. Innen lehet beszerezni

Térjünk a tárgyra

Ez a mai menet kicsit trükkösebb mint a múltkori, mivel mindent forrásból kell elkészíteni. A PyV8 wikin kívül hatalmas segítség volt a ez a bejegyzés, ez is, meg ez is. Ezúton is köszönet értük.

Előkészületek

Steril gépen szükség lehet ezekre a csomagokra:

$ sudo apt-get install build-essential subversion

Ha ez megvan akkor rakjuk fel a Python 3-as verzióját (3.4.2 volt a legutolsó amikor ezt a bejegyzést írtam). Ha kész a fordítás akkor készítsünk egy virtualenvet.

$ pyenv install 3.4.2
$ pyenv virtualenv 3.4.2 v8
$ pyenv shell v8

Hozzunk létre egy könyvtárat amibe a forrásokat pakolásszuk majd:

$ cd ~
$ mkdir v8
$ cd v8

Jöhetnek a források

$ svn checkout http://v8.googlecode.com/svn/trunk/ v8
$ svn checkout http://pyv8.googlecode.com/svn/trunk/ pyv8

Állítsuk be a V8_HOME környezeti változót, hogy a setup.py megtalálja majd a V8 forrását:

$ export V8_HOME=/home/slapec/v8/v8

A Boostra is szükség lesz, a nélkül nem fordul a PyV8. Jelenleg az 1.57.0-ás verzió a legújabb.

$ wget http://sourceforge.net/projects/boost/files/boost/1.57.0/boost_1_57_0.tar.gz
$ tar -xvzf boost_1_57_0.tar.gz

A boost_1_57_0.tar.gz-t törölhetjük is akár.

Boost fordítás

A Boosttal kezdünk. A 3.4.2-es verzió ellen kell lefordítani. Mivel a pyenv is minden alkalommal forrásból fordítja a feltelepített Python verziót, ezért a header fájlokat megtaláljuk majd a .pyenv könyvtárban.

$ cd boost_1_57_0
$ ./bootstrap.sh --with-python-version=3.4 
                 --with-python-root=/home/slapec/.pyenv/versions/3.4.2/ 
                 --with-python=/home/slapec/.pyenv/versions/3.4.2/bin/python

Az argumentumokat nem kell új sorba írni, csak az olvashatóság miatt írtam így őket. Fontos: a scriptnek abszolút elérési útvonalakat kell adni.

Ha kész a bootstrap akkor pedig:

$ ./b2 include="/home/slapec/.pyenv/versions/3.4.2/include/python3.4m"

És lehet menni ebédelni amíg ezen gondolkozik a számítógép.

A The Boost C++ Libraries were successfully built! üzenet után installáljuk a libraryt, frissítsük a cache-t és állítsuk be a BOOST_HOME környezeti változót hogy a boost könyvtárára mutasson:

$ sudo ./b2 install
$ sudo ldconfig
$ export BOOST_HOME=/home/slapec/v8/boost_1_57_0

V8 fordítás

A V8 fordítása kicsit trükkösebb. Egy patchet kéne alkalmazni a PyV8 setup.py fájljának, ami bekapcsolja a V8 RTTI és Exception támogatását. Ez a setup.py viszont nem Python 3 kompatibilis. Ez még a kisebb probléma.

A nagyobb probléma, hogy a V8 fordításához is szükség van a Pythonra, de ott is csak a 2-es verzió a megfelelő. Lassan 7 éve hogy megjelent az első Python 3 verzió, és még mindig ilyen gyakoriak a problémák.

Jobb ötlet híján úgy döntöttem, hogy a PyV8 setup scriptjét módosítom egy kicsit, hogy elvégezze a patchelést, de a V8 fordítását már nem, de kiírja a parancsot amivel a V8-at fordíthatom, amit aztán kézzel, Python 2 környezetben futtatok is.

Tehát a pyv8/setup.py fájlban két dolgot kell szerkeszteni:

Az 523. sorban ebből:

os.makedirs(build_path, 0755)

Ez lesz:

os.makedirs(build_path, 0o755)

Ugyanis Python 3-ban az octal literal is megváltozott.

Az 513. sorban ebből az egy sorból

exec_cmd(cmdline, "build v8 from SVN")

Ez a három lesz:

#exec_cmd(cmdline, "build v8 from SVN")
print('Now switch to a Python 2 environment and build V8 with the following command')
exit(cmdline)

(Természetesen az első print() elhagyható.)

Ezek után jöhet is a build.

$ python setup.py build

A script a végén kiírja a parancsot amivel a V8-at lefordíthatjuk és kilép.

Ha ez megvan, akkor váltsunk át egy Python 2-es környezetre (pyenv shell system ha az alapértelmezett interpreter ilyen, vagy egyszerűen nyissunk egy új shellt).

$ cd v8
$ make -j 8 disassembler=off vtunejit=off library=shared werror=no strictaliasing=on gdbjit=off debuggersupport=on extrachecks=off visibility=on regexp=native objectprint=off backtrace=on i18nsupport=off liveobjectlist=off snapshot=on verifyheap=off x64.release

(Természetesen mindenki a saját make parancsát használja)

Ameddig ez lefordul, addig megtárgyalhatjuk hogy nem-e gáz hogy a build scriptből félúton kiléptünk.

A buld osztályban a run() metódu a prepare_v8() függvényt hívja meg, itt látszódnak a V8 fordításának lépései. Feljebb a build_v8() függvényt írogattuk át kicsit, ez mellékesen az utolsó előtti lépés. A következő a generate_probes() lenne, ami nálam amúgy is meghal dtrace not found errorral, úgyhogy annak mindegy hogy lefut-e vagy sem (természetesen annyira nem mindegy, de ezzel a hibával egyelőre nem tudtam mit kezdeni).

Mostanra remélhetőleg elkészült a V8. Menjünk vissza a PyV8 könyvtárába, váltsunk vissza Python 3-ra (pyenv shell v8) és jöhet az install:

$ cd pyv8
$ python setup.py install

Ha szerencsénk van akkor Finished processing dependencies for PyV8==1.0-dev üzenet fogad. Már csak ki kell próbálni hogy működik-e a vadiúj librarynk :3

$ cd ~
$ python -m unittest PyV8
...
FAILED (failures=2)

Sajna 2 teszt elhasalt, dehát semmi se tökéletes, nem :) ?

Aftermath

Vannak megoldásra váró problémái a PyV8 projektnek, de én nagyon örülök neki, hogy dolgoznak rajta hogy Javascript kódot lehessen futtatni Python alól. Az a példányom, amit még a régi projektemhez fordítottam a mai napig jól működik, csak én már szeretnék Python 3-ra váltani, ezért voltam kénytelen újra elővenni ezt a problémát.

Mindenkinek kellemes fordítgatást : )

-slp

2013. december 26., csütörtök

Javascript futtatása Python kódból, PyV8 fordítása

Kellemes Ünnepeket mindenkinek!
Természetesen azért, mert megint nem írtam már hetek hónapok óta, még nem állt meg az élet. Mostanában web scraping-el foglalkozok, ami nagyon komplex egy feladat, persze csak ha az ember normálisan akarja megcsinálni.

Egy olyan problémába ütköztem, ami manapság egyre hétköznapibb, hogy a weboldal nagy részét Javascript kódból építik fel. Használhatnék amúgy erre egy valódi böngészőt is, ami futtatja a kapott kódot, és Python alól birizgálom, pl Selenium-al, de nekem csak a Javascript kódban tárolt adatokra van szükségem. Párperces keresgélés után nem találtam normális parsert a nyelvhez, így aztán úgy döntöttem, hogy felrakom a Google V8 engine-jét, a hozzá tartozó Python-os bindinggel, a PyV8-al, betápolom neki a kódot, és dolgozok a visszakapott értékekkel.

Szerencsére a kis Odroid-X2-n szépen -lassan- lefordul maga az engine, és a binding is. A dokumentációban nem nagyon van részletes leírás a fordítás menetéről, így ezt magamnak kellett kitalálnom. Szerencsére apt-ből mindent össze lehet szedni. Lássuk a parancsokat!
sudo apt-get install libboost-python-dev libboost-thread-dev libboost-system-dev
Ennyi az összes függőség a fordításhoz. Ha nincs svn kliens feltelepítve, akkor erre még szükség lehet:
sudo apt-get install subversion
Ezek után egyszerűen pip-el telepítjük
sudo pip install pyv8

Rövid példácska

Rendbe, ha már ilyen szépen feltelepült a lib, akkor azt is leírom hogy mire kellett.
Tehát, a feldolgozandó kódban, egyes HTML tag-ek onmouseover eventjén van az alábbihoz hasonló kód:
jQuery("#1337").html("Leet");
Azaz rámutatáskor ad 1337 id-ű tag tartalma legyen a Leet string. Ilyen egyszerű.

Nem akartam az egész jQuery-t betölteni a memóriába, ezért inkább írtam egy kis kamu függvényt, amivel a fenti sor futtatható. Így néz ki:
var jQuery = function (unused){
    html = function(data){
        return data
    }
    return this;
}
A jQuery függvény visszatér a this-el, ami úgy mellékesen a window. Maga a függvény nem csinál semmit se a paraméterével. A függvény törzsében definiálunk egy html nevű függvényt. Ez globális lesz, így a window.html-en keresztül is elérhető.
Tehát amikor a jQuery visszatér a this-el, ott lesz már a window.html is, így működni fog method chaining (igazából nem fog működni, ahhoz megint csak this-el kellett volna visszatérni, de nekem most így is megfelel a kód).

Rakjuk össze az egészet és próbáljuk ki:

import PyV8
ctx = PyV8.JSContext()
ctx.enter()
ctx.eval('var jQuery=function(unused){html=function(data){return data};return this;}')
ctx.eval("jQuery('#1337').html('Leet')")
#>>>'Leet'

Először létrehozunk egy új contextet és bele is lépünk. Ez után betöltjük a függvényt, aztán kipróbáljuk hogy visszakapjuk-e a html-nek adott stringet, természetesen a Python interpreterben.
Az utolsó sorban látszik hogy ez megtörtént, úgyhogy nagy az öröm!

-slp