[Update] Az IMDB lapok forrása megváltozott de egyenlőre vannak olyan rendesek, hogy a php kérésekre a régi forrást adják vissza ezért a minta alkalmazás még működik. Ez azzal is jár, hogy az oldal jelenlegi struktúrájának elemzésekor nem lesznek megtalálhatók a lent említett HTML elemek.
A segédletben használt Transformers film oldalának forrása innen letölthető. [/Update]
[Update2] Sajnos már a php kérésére is az új források jönnek vissza, így az alkalmazás csak a nem változó elemeket, a címet és az évet fogja felismerni. A segédlet alapján, így már akinek van kedve kísérletezhet az új elemző elkészítésével, legyen ez a gyakorló feladat
[/Update2]
Mivel regex segédlet az interneten számos helyen van most nem ennek elsajátításán lesz a fő hangsúly, hanem hogyan lehet egyszerűen flash-ben használni és adatokat kiszűrni a segítségével.
Az illesztési minták elkészítéséhez nagy segítséget nyújthat Grant Skinner RegExr alkalmazása amit itt találhatunk meg: http://gskinner.com/RegExr/. Számos előnyeinek egyike, hogy flash-be bemásolható formában is kiírja a mintákat lehet tesztelni működésüket vagy akár flash-ből bemásolva is tudja használni őket.
Hasznos lehet továbbá FireBug is aminek segítségével egyszerűen elemezhetjük egy weblap felépítést: http://getfirebug.com/
AS3-ban a reguláris kifejezéseket lehetőség van speciális módon, a kódban definiálni hasonlóan az XML-hez, így nincs szükség az amúgy stringekben speciális jelentéssel bíró karaktereket mint pl az escape karakter “\” duplázására.
private var pattern1:RegExp = new RegExp(" hello \\s world ","gimsx");
private var pattern2:RegExp = / hello \s world /gimsx;
private function test():void
{
var foo:String = "Hey and Hello World how are you?"
if(pattern2.test(foo)){
trace("Benne van")
pattern2.lastIndex = 0
var result:Array = pattern2.exec(foo)
trace("index: "+result.index)
trace("input: "+result.input)
trace(result[0])
}else{
trace("Nincs benne")
}
}
eredmény
Benne van index: 8 input: Hey and Hello World how are you? Hello World
Az 1. sorban a hagyományos módon hoztam létre a RegExp-et a 2. sorban az új AS3-as szintaxis segítségével. A továbbiakban ezt fogom mindenhol használni az egyszerűsége és átláthatósága miatt. Ahogy a kódból is látszik / jelek közé kerül a kifejezés, a végére pedig a különböző beállítások amik a következőket jelentik:
| Flag | Tulajdonság | Leírás |
|---|---|---|
| g | global | Több találatot ad vissza nem csak addig keres amíg 1-et nem talál |
| i | ignoreCase | Nagy és kis betők között nem tesz különbséget de fontos hogy ez csak az angol A-Z a-z karakterekre igaz. |
| m | multiline | Az $ és a ^ karakter ebben az estben több sor esetén egy sor elejét és végét is jelölheti. |
| s | dotall | A . (pont) karakter az új sor karakterre is illeszkedik, fontos ha több soros szövegben keresünk. |
| x | extended | A kifejezésbe lehetőség van szóközöket rakni nem lesznek figyelembe véve így lehetőség van formázni és átláthatóvá tenni azt. |
A test segítségével tesztelhetem hogy van-e egyezés, majd az exec segítségével lekértem az eredményt ami tartalmaz pár speciális tulajdonságot. Az ne zavarjon meg senkit hogy a dokumentációban Object a visszatérési értéke, tulajdonképpen egy Array-t ad vissza amin speciális tulajdonságok is elérhetőek. A mintában csak tesztelés után hívtam meg az exec-et hogy szemléltessem a 2 metódus működését, erre amúgy nincs szükség, ha nincs egyezés exec null értékkel tér vissza. A pattern2.lastIndex = 0 sorra azért van szükség, mivel ha újra akarunk használni egy példányt minden illesztés után a pattern elmenti önmagán az utolsó pozíciót, így több találat estén onnan folytatja a keresést, de mivel csak egy találtunk volt a következő illesztésnél már nem találna semmit attól a pozíciótól kezdve.
Most hogy tudjuk hogyan kell definiálni a reguláris kifejezéseket írjuk meg az IMDB oldalához tartozókat. A forrás elemzéséhez válasszunk egy népszerű filmet amin minden információ megtalálható, legyen az mondjuk a Transformers.
Kezdjük a film címével és az évvel amikor megjelent. Ez az információ mind megtalálható a<title> elemek között estünkben ez így néz ki:
html:
<title>Transformers (2007)</title>
regex:
/<title>(?P<title>.+) \s* \( (?P<year>\d{4}) \) .* <\/title>/ixs
leírás:
/<title>{egy vagy több karakter} {0 vagy több szóköz} \( {4 karakteres szám} \) .* <\/title>/gixsm
Lehetőség van névvel ellátott csoportok definiálásra amit a (?P<név> … ) formátumban hozhatunk létre majd később a csoport által illesztett szöveget a név segítségével tudjuk visszakérni. Most már csak annyi a dolgunk, hogy megírjuk a szükséges függvényeket.
private static function getTitleInfo(source:String):TitleInfo
{
titleRegex.lastIndex = 0
var result:Array = titleRegex.exec(source)
var info:TitleInfo
if(result){
info = new TitleInfo()
info.title = unescapeEntity(result.title)
info.year = parseInt(result.year)
}
return info
}
private static function unescapeEntity(str:String):String
{
if(str){
return new XMLDocument(str).firstChild.nodeValue;
}else{
return str
}
}
A getTitleInfo függvényem megkapja a forrást és ráilleszti a regex-et ami ha sikeres a névvel ellátott csoportokat a title és a year tulajdoonságokkal lekérhetem. Ezeket egy TitleInfo osztályba mentem ami csak ezt a 2 tulajdonságot definiálja. Fontos odafigyelni hogy a forrás tartalmazhat HTML karakter entitás referenciákat is ami azt jelenti hogy például a Gálvölgyi Show forrásban így néz ki:
<title>"Gálvölgyi Show" (1998)</title>
Ezeket egy trükkel tudjuk visszakonvertálni amit az unescapeEntity függvény implementál.
Lépjünk tovább a következő információ amire szükségünk van a szavazatok és az érékelés amikre a ez kifejezés illeszkedik:
html:
<div class="starbar-meta">
<b>7.3/10</b>
<a href="ratings" class="tn15more">190,003 votes</a> »
regex:
/<div\sclass="starbar-meta"> .*? <b> (?P<score>( \d{1,2}) | (\d\.\d)) \/ 10 <\/b> .*?> (?P<votes>.+?) \s votes<\/a>/ixs
leírás:
/<div class="starbar-meta"> .*? <b> ({1 vagy 2 szám} VAGY {szám . szám}) / 10 </b> .*? > {több karakter} {space} votes<\/a>/ixs
Ebben a forrásban használtam egy úgynevezett lusta kvantort a mi a fenti kódban .*? ként látható. Ha egy kérdő jelet rakok a * mögé az azt fogja jelenteni hogy a lehető legkevesebb köztes karaktert keresse meg az előtte és utána lévő kifejezések között. Ez nagyon fontos mivel különben nem a következőre hanem akár jóval messzebb található egyezésre fog illeszkedni, ami már nekünk nem megfelelő hiszen bizonyos sorrendben egymás után megtalálható html elemeket keresek. Most lássuk a kódot:
private static function getScoreInfo(source:String):ScoreInfo
{
var floatRegex:RegExp = /(\d+) (,|\.) (\d+)/x
ratingRegex.lastIndex = 0
var result:Array = ratingRegex.exec(source)
var info:ScoreInfo
if(result){
info = new ScoreInfo()
info.score = parseFloat(result.score)
info.votes = parseInt(
String(result.votes).replace(floatRegex, "$1$3")
)
}
return info
}
A fenti kódban a lekérés mellett a String.replace metódusát is használtam ami nem csak String hanem RegExp típusú paramétert is képes fogadni. Ebben az esetben a reguláris kifejezés által megadott, egy vagy több szövegrészt fogja cserélni a 2. paraméterben megadott szöveggel, vagy akár egy megadott függvény visszatérési értékével.
Ha az első paraméter RegExp és a második paraméter String, akkor a String tartalmazhat bizonyos speciális karaktereket amiket a csere után helyettesít, ezeket a függvény leírásánál meg lehet nézni. Én a függvényben a $n formátumot adtam meg ahol az n a csoport sorszáma amiből 3-at definiáltam a reguláris kifejezésben. Ezzel azt értem el hogy a . vagy , előtti és utáni szöveget a . vagy , kihagyásával illesztem össze így IMDB-ben a vesszővel elválasztott szavazat számot vessző nélkül számmá tudom konvertálni.
Következő feladatunk a műfaj (Genre) információk lekérése amiből több is van, de a leírás kedvéért nehezítsük a dolgunkat és mi csak azokra vagyunk kíváncsiak amik a “Genre:” utáni részben vannak. Ehhez a legegyszerűbb ha először megkapjuk annak a résznek a forrását, majd abban keressük meg a műfaj neveket.
html:
<div class="info"> <h5>Genre:</h5> <div class="info-content"> <a href="/Sections/Genres/Action/">Action</a> | <a href="/Sections/Genres/Sci-Fi/">Sci-Fi</a> | <a href="/Sections/Genres/Thriller/">Thriller</a> <a class="tn15more inline" href="/title/tt0418279/keywords" onClick="(new Image()).src='/rg/title-tease/keywords/images/b.gif?link=/title/tt0418279/keywords';">See more</a> » </div> </div>
műfaj div regex:
/<h5>Genre:<\/h5> .*? <div.*?> (?P<div>.*?) <\/div>/gixsm
műfaj regex:
/<a \s href= "\/Sections\/Genres\/.*?" > (?P<genre>.*?) <\/a>/gixsm
kód:
private static function getGenres(source:String):Array
{
genresDivRegex.lastIndex = 0
var result:Array = genresDivRegex.exec(source)
var genres:Array = new Array()
if(result){
source = result.div
genreRegex.lastIndex = 0
while(result = genreRegex.exec(source)){
genres.push(result.genre)
}
}
return genres
}
A getGenres függvényem először lekéri a div forrását ahol a műfajok fel vannak sorolva, majd egy while ciklus addig kéri le őket amíg null-t nem kapunk. Ebben az esetben a cikluson belül nem szabad nullázni az indexet mivel szükségünk van rá, hogy mindig a következőt kapjuk és nem akarunk végleten ciklusba sem kerülni.
Most hogy sikeresen megírtuk a főbb információkat lekérő elemeket, lássuk hogyan néz ki az egész összerakva. Készítsünk egy statikus osztályt ami visszaadja a forrásból kiszűrt információkat, és egyet amiből az információk könnyen elérhetőek.
IMDBSourceInfo.as
package
{
public class IMDBSourceInfo
{
public var hasRating:Boolean
public var title:String
public var year:int
public var genres:Array
public var score:Number
public var votes:Number
public var posterUrl:String
}
}
IMDBSourceParser.as
package
{
import flash.xml.XMLDocument;
public class IMDBSourceParser
{
private static const titleRegex:RegExp =
/<title>(?P<title>.+) \s* \( (?P<year>\d{4}) \) \s* <\/title>/gixsm
private static const genresDivRegex:RegExp =
/<h5>Genre:<\/h5> .*?<div.*?> (?P<div>.*?) <\/div>/gixsm
private static const genreRegex:RegExp =
/<a \s href= "\/Sections\/Genres\/.*?" > (?P<genre>.*?) <\/a>/gixsm
private static const posterRegex:RegExp =
/<a\s*?name=\"poster\" .*? src=\"(?P<url>.*?)\" \s*? \/>/ixs
private static const ratingRegex:RegExp =
/<div\sclass="starbar-meta"> .*? <b> (?P<score>( \d{1,2}) | (\d\.\d)) \/ 10 <\/b> .*?> (?P<votes>.+?) \s votes<\/a>/ixs
//--------------------------------------------------------------------------
//
// Public Static Methods
//
//--------------------------------------------------------------------------
public static function parse(source:String):IMDBSourceInfo
{
var data:IMDBSourceInfo = new IMDBSourceInfo()
var titleInfo:TitleInfo = getTitleInfo(source)
var scoreInfo:ScoreInfo = getScoreInfo(source)
if(titleInfo){
data.title = titleInfo.title
data.year = titleInfo.year
}
if(scoreInfo){
data.score = scoreInfo.score
data.votes = scoreInfo.votes
data.hasRating = true
}
data.genres = getGenres(source)
data.posterUrl = getPoster(source)
return data
}
//--------------------------------------------------------------------------
//
// Private Static Methods
//
//--------------------------------------------------------------------------
private static function getPoster(source:String):String
{
posterRegex.lastIndex = 0
var result:Array = posterRegex.exec(source)
if(result){
return result.url
}
return null
}
private static function getScoreInfo(source:String):ScoreInfo
{
var floatRegex:RegExp = /(\d+) (,|\.) (\d+)/x
ratingRegex.lastIndex = 0
var result:Array = ratingRegex.exec(source)
var info:ScoreInfo
if(result){
info = new ScoreInfo()
info.score = parseFloat(result.score)
info.votes = parseInt(
String(result.votes).replace(floatRegex, "$1$3")
)
}
return info
}
private static function getGenres(source:String):Array
{
genresDivRegex.lastIndex = 0
var result:Array = genresDivRegex.exec(source)
var genres:Array = new Array()
if(result){
source = result.div
genreRegex.lastIndex = 0
while(result = genreRegex.exec(source)){
genres.push(result.genre)
}
}
return genres
}
private static function getTitleInfo(source:String):TitleInfo
{
titleRegex.lastIndex = 0
var result:Array = titleRegex.exec(source)
var info:TitleInfo
if(result){
info = new TitleInfo()
info.title = unescapeEntity(result.title)
info.year = parseInt(result.year)
}
return info
}
private static function unescapeEntity(str:String):String
{
if(str){
return new XMLDocument(str).firstChild.nodeValue;
}else{
return str
}
}
}
}
class ScoreInfo
{
public var votes:Number
public var score:Number
}
class TitleInfo
{
public var title:String
public var year:Number
}
Most hogy minden szükséges osztály megvan meg kell írnunk a forrás betöltését. A forrásokat az URLLoader segítségével tudjuk betölteni, de viszont ha a webhely ahonnan a forrásokat töltenénk nem engedélyezi a hozzáférést nekünk egy crossdomain.xml-ben akkor php-val kell betöltenünk a forrást majd azt átadni flash-nek. IMDB-n noha van ilyen xml file http://www.imdb.com/crossdomain.xml nekünk nem ad engedélyt.
Amennyiben a betöltést fejlesztő környezetben teszteltjük nem lesz szükségünk ilyen php-ra mivel ezekre az swf-ekre nem vonatkoznak a biztonsági megkötések, viszont amint kikerülnek internetre, nem fognak működni.
Szükségünk van tehát egy php-ra ami letölti a kívánt adatot majd átadja nekünk.
getsource.php
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$url = $_GET["url"];
$page = file_get_contents($url);
echo $page;
?>
A php egyszerűen lekéri a GET metódussal megkapott elérési útvonalon található weblap forrását és ezt fogjuk majd betölteni. Beállítunk továbbá pár fejlécet ami azt fogja megakadályozni hogy a böngésző a cache-ből töltse be az adatokat. Éles körülmények között nem árthat levédeni a php-t hogy csak a kívánt feladatokat lássa el.
Most hogy minden szükséges komponens a rendelkezésünkre áll, töltsük be és értelmezzük az adatokat.
private var urlLoader:URLLoader
private function init():void
{
urlLoader = new URLLoader()
urlLoader.addEventListener(Event.COMPLETE, onLoadComplete)
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onLoadFailed)
urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoadFailed)
}
private function loadSource(url:String):void
{
var req:URLRequest = new URLRequest(
"http://localhost/getsource.php?url="+escape(url)
)
urlLoader.load(req)
}
private function onLoadComplete(event:Event):void
{
var info:IMDBSourceInfo =
IMDBSourceParser.parse(urlLoader.data)
trace("Cím:", info.title)
trace("Év:", info.year)
trace("Műfaj:", info.genres)
trace("Szavazatok:", info.votes)
trace("Értékelés:", info.score)
}
private function onLoadFailed(event:ErrorEvent):void
{
trace("Hiba", event.text)
}
Az init és a loadSource meghívása után sikeres betöltés esetén meg kell kapnunk a kívánt információkat.
Források: IMDBTutorial.zip
Alkalmazás forrás nézete: srcview/SourceView.html





Úgy néz ki egyszerűen megoldódott a probléma a képet is a már megírt php segítségével kell betölteni. A minta alkalmazást frissítettem most már elvileg működik minden böngészőben.
Nagy valószínűséggel a probléma a böngészők Http kérés fejléceire vezethető vissza, ugyanis IE és Chrome is elküldi a kéréssel azt hogy egy swf file-tól jött az és erre IMDB media úgy néz ki érzékeny tehát nem adja vissza a képet. Ha lesz egy kis időm frissítem a leírást a megoldással addig is mindenki próbálja újabb Firefox alatt ott úgy tűnik nincs ilyen probléma.
Hasznos segédlet, főleg, hogy nagy imdb fan vagyok. A lekérés sikeres a fenti példában de a képet nem tölti le…