Научете основите на 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“ и „официалното справочно ръководство“.

  1. Коментарите в Lua започват с две тирета (--), както в SQL.
  2. Lua е чувствителен към главни и малки букви.
  3. Няма нужда да декларирате променливи в Lua преди достъп до тях. Променливите по подразбиране са глобални, но могат да бъдат променени на локални, като ги декларирате с ключовата дума local.
  4. Lua е динамично въвеждане, което означава, че типовете се извеждат от стойностите, както в Python. Не е необходимо (и не можем) да указваме типовете, когато декларираме променливи.
  5. Низ може да бъде създаден с единични, двойни кавички или двойни къдрави скоби ([[]]). Двойните къдрави скоби се използват за писане на многоредови низове, които се използват много често в Splash, тъй като JavaScript кодът обикновено се пише като многоредови низове.
  6. nil е подобно на None в Python. Въпреки това, той прави повече от това да служи като празна или недефинирана стойност в Lua. Променлива, чиято стойност е nil, ще събира боклука в Lua.
  7. Само false и nil са фалшиви в Lua, а всяка друга стойност е вярна, включително 0 и празни низове.
  8. Функциите са първокласни стойности в Lua, подобни на тези в Python, което означава, че функциите могат да се използват по същия начин като другите типове данни. Те могат да се съхраняват в променливи, да се предават на друга функция или да се връщат от друга функция.
  9. Таблицата е единствената структура от данни в Lua. Няма други структури от данни като списък/масив, речник/обект и т.н., които обикновено се срещат в други езици. Въпреки това, всички други структури от данни могат да бъдат конструирани въз основа на таблици, както ще видим по-късно.
  10. Когато дадена функция се извиква с низ или таблица, скобите могат да бъдат пропуснати. Това може да е объркващо за начинаещи.
  11. Таблиците могат да се третират като обекти и могат да имат методи. Методите могат да бъдат извикани или с точка (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 уеб страници в по-големи подробности.

Свързани статии