Научете основите на Lua за уеб скрапинг като разработчик на Python
Започнете с основните неща на Lua за 10 минути
Lua е лек език за програмиране на високо ниво, който обикновено се използва за целите на скриптовете. Той е проектиран с намерението да бъде интегриран в други приложения и позволява на разработчиците да разширят функционалността на своя софтуер чрез персонализирани скриптове.
Като разработчик на Python обикновено може да нямате шанса да работите с Lua. Въпреки това, ако трябва да скрейпвате уеб страници на JavaScripe в работата си, ще имате голям шанс да го използвате поради Splash, лека и скриптова машина за браузър, разработена от Zyte (преди това Scrapinghub), същата компания, която разработва Скрепи.
Lua се използва в Splash като скриптов език за осигуряване на по-усъвършенстван контрол върху процеса на уеб скрапиране. С Lua скриптове можете да взаимодействате с уеб страници, да манипулирате DOM, да изпълнявате разширен JavaScript и т.н. В тази публикация ще представим основите на Lua, които са от съществено значение за уеб скрапинг с помощта на Splash. След това ще можете да разбирате Lua скриптове в Splash и можете да започнете да пишете скриптове сами.
Инсталирайте Lua
Всъщност за тази публикация не е необходимо да инсталираме Lua, можете просто да опитате командите в Lua Live Demo. Въпреки това, ако искате да стартирате Lua на собствения си компютър локално, можете просто да изтеглите изходния код и да го компилирате, както е показано тук.
Основен синтаксис
Тази част не е предназначена да бъде изчерпателна и ще покрие само най-важното, което много вероятно ще бъде необходимо в Splash скриптовете. За по-изчерпателно въведение се препоръчват книгата „Програмиране на Lua“ и „официалното справочно ръководство“.
- Коментарите в Lua започват с две тирета (
--
), както в SQL. - Lua е чувствителен към главни и малки букви.
- Няма нужда да декларирате променливи в Lua преди достъп до тях. Променливите по подразбиране са глобални, но могат да бъдат променени на локални, като ги декларирате с ключовата дума
local
. - Lua е динамично въвеждане, което означава, че типовете се извеждат от стойностите, както в Python. Не е необходимо (и не можем) да указваме типовете, когато декларираме променливи.
- Низ може да бъде създаден с единични, двойни кавички или двойни къдрави скоби (
[[]]
). Двойните къдрави скоби се използват за писане на многоредови низове, които се използват много често в Splash, тъй като JavaScript кодът обикновено се пише като многоредови низове. nil
е подобно наNone
в Python. Въпреки това, той прави повече от това да служи като празна или недефинирана стойност в Lua. Променлива, чиято стойност еnil
, ще събира боклука в Lua.- Само
false
иnil
са фалшиви в Lua, а всяка друга стойност е вярна, включително 0 и празни низове. - Функциите са първокласни стойности в Lua, подобни на тези в Python, което означава, че функциите могат да се използват по същия начин като другите типове данни. Те могат да се съхраняват в променливи, да се предават на друга функция или да се връщат от друга функция.
- Таблицата е единствената структура от данни в Lua. Няма други структури от данни като списък/масив, речник/обект и т.н., които обикновено се срещат в други езици. Въпреки това, всички други структури от данни могат да бъдат конструирани въз основа на таблици, както ще видим по-късно.
- Когато дадена функция се извиква с низ или таблица, скобите могат да бъдат пропуснати. Това може да е объркващо за начинаещи.
- Таблиците могат да се третират като обекти и могат да имат методи. Методите могат да бъдат извикани или с точка (
obj.method()
), или с двоеточие (obj:method()
). Последното е синтактична захар заobj.method(obj)
. Това се използва много често в Splash и ще бъде представено по-подробно по-късно.
След това допълнително ще илюстрираме някои части, които се нуждаят от допълнително въведение с някакъв прост код.
Променливи
Трябва да се подчертае, че глобалните променливи не трябва да бъдат декларирани, а локалните променливи все още трябва да бъдат декларирани. В противен случай те ще бъдат глобални, дори когато са създадени във функция:
function testVariables() var1 = 100 local var2 = 200 print(var1, var2) end testVariables() -- 100, 200 print(var1, var2) -- 100, nil
Както виждате, var1
е глобална променлива, въпреки че е създадена във функция. Като най-добра практика ние винаги декларираме променливите като локални, освен ако не трябва да се използват глобално.
Функции
Както бе споменато по-горе, функциите са първокласни стойности в Lua, което означава, че могат да се съхраняват в променливи, да се предават на друга функция или да се връщат от друга функция.
Функция може да бъде създадена директно с ключовата дума function
:
function echo(var) print(var) end
Може също да се създаде анонимно и след това да се присвои на променлива:
echo = function (var) print(var) end
Всъщност първото може да се разглежда като синтактична захар за второто. По-видно е, когато създавате функции за таблица:
obj = {} function obj.echo(var) print(var) end -- Above is the same as: obj.echo = function (var) print(var) end -- We can call both with the same syntax: obj.echo(100) -- 100
Затваряния
Много често срещана концепция в Lua е затварянето, което всъщност е функция, върната от друга функция. Важна характеристика на затварянето е, че може да запомни и актуализира променливите, предадени от родителската функция.
Нека го видим в прост пример:
function createAdder(initVal) local value = initVal or 0 -- This is the way to set default value in Lua. return function (num) value = value + num print(value) end end adder = createAdder() adder(1) -- 1 adder(2) -- 3 adder100 = createAdder(100) adder100(1) -- 101 adder100(2) -- 103
Затварянията също демонстрират, че функциите в Lua са първокласни стойности и могат да бъдат върнати от друга функция.
Масиви
Както бе споменато по-горе, единствената структура от данни в Lua е таблица и няма такава структура от данни на масив или списък. Таблиците обаче могат да се използват за създаване на масиви в Lua. Просто трябва да поставите отделни стойности във къдрави скоби, подобно на създаването на набори в Python:
arr = {"red", "green", "blue"}
И тогава можем да получим достъп до стойностите по индекс. Имайте предвид обаче, че за разлика от повечето други езици за програмиране, индексът започва от 1 за Lua!
print(arr[1]) -- red print(arr[3]) -- blue
Под капака, масивите в Lua все още са асоциативни масиви, които са колекции от двойки ключ-стойност, където всеки ключ е свързан с конкретна стойност. Горният масив е същият като:
arr = {[1]="red", [2]="green", [3]="blue"}
Имайте предвид, че индексите трябва да бъдат поставени в квадратни скоби, ако са посочени изрично.
Можем да използваме цикъла for, за да итерираме елементите от масив. Нека дефинираме функция, която може да свърши такава работа и да отпечата масива по приятен начин:
function printArr(arr) if not arr then print(arr) -- nil return end repr = '[' for _, v in ipairs(arr) do repr = repr .. tostring(v) .. ', ' end repr = string.gsub(repr, ",%s*$", "") .. ']' print(repr) end arr = {"red", "green", "blue"} printArr(arr) -- [red, green, blue]
Този прост пример съдържа няколко често използвани точки на знания за Lua:
- Обърнете внимание на синтаксиса на условието
if
и цикълаfor
в Lua. Трябва да използвамеthen … end
илиdo … end
изрично, за да обозначим кодов блок в Lua. ipairs()
връща двойките индекс/стойност на масив. Тъй като тук не използваме индекса, той е присвоен на фиктивна променлива (_
), която е същата като в Python...
се използва за свързване на низове в Lua. Стойностите, които не са низове, ще бъдат преобразувани в низове с помощта на функциятаtostring()
преди конкатенацията.- Функцията
string.gsub()
търси модел в низова променлива и го заменя със заместващ низ. Моделът е подобен на регулярните изрази и в повечето случаи работи по същия начин.
Като странична бележка, можем да получим дължината на масив с хеш оператора (#
) и по този начин можем да преминем през него, използвайки числовия цикъл for:
arr = {"red", "green", "blue"} -- Note that the range includes both ends. for i = 1, #arr do print(arr[i]) end -- red -- green -- blue
Можем да използваме table.insert()
и table.remove()
, за да вмъкнем или премахнем елемент в масив:
table.insert(arr, "black") -- Inserted in the end. table.insert(arr, 2, "pink") -- Insert at a specific position. printArr(arr) -- [red, pink, green, blue, black] arr.remove(arr) -- Remove the last item. arr.remove(arr, 3) -- Remove the item at a specific position. printArr(arr) -- [red, pink, blue]
Асоциативни масиви
Асоциативен масив в Lua е колекция от двойки ключ-стойност, където всеки ключ е свързан с конкретна стойност. Подобно е на речниците в Python. Въпреки това, от техническа гледна точка, той е по-подобен на обектите в ванилия JavaScript.
Първо, ако ключовете са низове, които също са валидни имена на идентификатори в Lua, можем да ги използваме директно като ключове и няма нужда да използваме кавички и квадратни скоби. Това е и най-честият случай на употреба:
myTable = {value=100}
Технически е същото като:
myTable = {['value']=100}
Когато ключовете са променливи, числа, запазени ключови думи като if
и for
или низове, които не са валидни като имена на идентификатори в Lua, те трябва да бъдат поставени в квадратни скоби:
myTable= { value=100, [1]='color', ['if']=true, ['1stName']='John', ['last name']='Doe' }
Когато ключът е низ, който също е валидно име на идентификатор, можем да получим достъп до неговата стойност или с помощта на точка, или чифт квадратни скоби:
print(myTable.value) -- 100 print(myTable["value"]) -- 100 print(myTable[value]) -- nil
Имайте предвид, че третият връща nil
. Това е така, защото стойността на променливата value
се използва като ключ, който е nil
. nil
не съществува като ключ в таблицата и всъщност не е разрешен. Ако обаче във вашия код има променлива, наречена value
, може да получите неочаквани резултати.
За други типове ключове винаги трябва да използвате квадратни скоби за достъп до стойността:
print(myTable[1]) -- color print(myTable['if']) -- true print(myTable['1stName']) -- 'John' print(myTable['last name']) -- 'Doe'
Можем да преминем през двойките ключ/стойност на таблица с помощта на функцията pairs()
. Имайте предвид, че ключовете не са подредени и може да са различни в последователността, когато са създадени:
for k, v in pairs(myTable) do print(k .. ' -> ' .. tostring(v)) end -- value -> 100 -- last name -> Doe -- 1 -> color -- if -> true -- 1stName -> John
Класове и обекти
Lua не е роден език за обектно-ориентирано програмиране (ООП) и следователно няма такива концепции като класове или обекти. Всичко (класове или обекти) е просто таблици в Lua. ООП обаче може да се реализира лесно с таблици.
Първо, една таблица вече може да се разглежда като обект и можем да добавяме функции към нея, както видяхме по-рано. Демонстрираният по-горе е като статичен метод, който не изисква екземпляр. Можем също така да създадем класически методи за екземпляри, които изискват екземпляр. Може да се реализира с „магическото“ двоеточие в Lua:
person = {firstName = "John", lastName = "Doe"} function person:getFullName() return self.firstName .. ' ' .. self.lastName end print(person:getFullName()) - John Doe
В този пример self
се отнася до самия обект, извикващ функцията, подобно на self
в Python.
Декларацията на функцията, използваща двоеточие, е просто синтактична захар за следната декларация (да, има много захари 🍬 в Lua):
person = {firstName = "John", lastName = "Doe"} function person.getFullName(self) return self.firstName .. ' ' .. self.lastName end print(person.getFullName(person))
Разбирането на тази синтактична захар е много важно за разбиране на класовете, инстанцията и наследяването в Lua. Нека го демонстрираме с прост пример:
-- Create a class, which is just a table in Lua. Animal = {} -- Create a constructor function for the class: function Animal:new() local newAnimal = {} -- Create a metadata table which can be associated with another table to customize its behavior. local metatable = {} metatable.__index = self setmetatable(newAnimal, metatable) return newAnimal end -- Create an instance method. function Animal:breathe() print("I'm breathing...") end -- Create an instance of the Animal class. animal = Animal:new() -- Call an instance method. animal.breathe() -- I'm breathing...
Когато използвате таблици като класове в Lua, има две много важни концепции, а именно метатаблица и метаметод.
В Lua метатаблицата е специална таблица (е, това е просто обикновена таблица, но се използва за специални цели), която може да бъде свързана с друга таблица, за да персонализира поведението си или да предостави резервни варианти за несъществуващи ключове. Метаметодите са функции/методи, дефинирани в метатаблицата, които могат да осигурят претоварване на оператора или прилагане на наследяване.
Най-важният метод е __index
, който може да приеме функция с таблицата като първи параметър и достъпният ключ като втори. Следователно подробна версия на конструктора може да бъде написана като:
-- Create a constructor function for the class: function Animal:new() local newAnimal = {} local metadataTable = {} metadataTable.__index = function (_, key) return self[key] end setmetatable(newAnimal, metadataTable) return newAnimal end
Предадената таблица (тук newAnimal
) не се използва и следователно може да бъде заменена с фиктивната променлива _
.
За създаване и наследяване на клас, използването на метаметода __index
е толкова обичайно, че Lua предоставя пряк път. Въпреки че __index
се нарича метаметод, той може да приеме таблица като стойност, както е показано по-горе. Това е друга синтактична захар за многословната версия по-горе.
Всъщност, тъй като функцията setmetatable()
връща таблицата обратно, можем да опростим конструктора, както следва, което се използва много често на практика:
-- Create a constructor function for the class: function Animal:new() local newAnimal = {} return setmetatable(newAnimal, {__index = self}) end
Метаметодът __index
се присвоява на метатаблица, създадена в движение.
Със знанията по-горе, наследяването на класове е по-лесно за разбиране:
-- Well, in Lua, an instance of a class can be treated as another class, and -- it's still just a table... -- It inherits all the properties and methods of its parent. Bird = Animal:new() function Bird:fly() print("I can fly!") end bird = Bird:new() -- Inherits from Animal. bird.breathe() -- Also inherits from Animal. bird.fly() -- New in the Bird class
Свойство/метод ще бъде проверено в текущия екземпляр, класа, родителския клас, баба и дядо и т.н., което от двете е първото, което има даденото свойство/метод. Ако нито един не може да бъде намерен във всички тях, се връща nil
.
Пример за начален скрипт
И накрая, нека проверим прост пример за Splash от официалния документ, който ще бъде доста лесен за разбиране сега:
function main(splash, args) splash:go("http://example.com") splash:wait(0.5) local title = splash:evaljs("document.title") return {title=title} end
Както виждате, дебелото черво се използва много силно в Splash. Може да е загадъчно, ако не познавате Lua. Но със знанието за тази публикация би трябвало да ви е много удобно да работите с нея сега.
Ще бъдат публикувани някои допълнителни публикации за това как да използвате Lua скриптове в Splash за изтриване на JavaScript уеб страници в по-големи подробности.