Tag Archives: Павутина

Скільки слів треба щоб написати вікіпедію? І які зустрічаються частіше?

Виявляється відтоді як я рахував символи вікіпедії пройшло вже більше року. Рахував я їх за допомогою Go, хоча можна було сильно спростити собі життя і рахувати їх за допомогою Python і pywikipediabot. Сьогодні розкажу як, і як можна побачити з назви – рахуватимемо слова.

Я чомусь боявся що щоб порахувати слова пам’яті не вистачить, тому треба якусь базу даних. Або пробувати все в пам’яті, але аби комп’ютеру не стало погано якось обмежити доступну пам’ять. Але мої 4Gb використовувались лише щось трохи більше ніж на 40% для підрахунку всіх слів включно зі сторінками обговорень, категорій, шаблонів, сторінок опису файлів, і т.п. німецької вікіпедії.

В модулі pywikibot.pagegenerators є об’єкти XMLDumpOldPageGenerator і XMLDumpPageGenerator. Вони приймають назву архіву з XML дампом в конструкторі, а після створення по них можна ітеруватися отримуючи об’єкти сторінки. Не ведіться на слово “Old” в назві першого об’єкта, це означає не депрекацію, а те що текст сторінки буде братись з дампа, а в другому випадку з дампа буде братись лише заголовки, а за свіжим текстом зроблять запит, що сповільнить обробку разів в 500. Тобто замість кількох годин ви будете чекати рік. 🙂

Я спробував проаналізувати німецьку вікіпедію (код буде пізніше), на це пішло 694 хв (трохи менше ніж 12 годин) і вийшло що в ній на 6,425,028 сторінках використовується 2,381,457,397 слів (приблизно 371 на сторінку), з них різних слів 18,349,393. В кінцевому результаті CSV з частотним словничком виходить на 300MB.

Серед тих що зустрічаються лише раз є слова типу PikettdienstPikettdienst (помилка парсера який видаляв розмітку), слово – це юридичний термін швейцарської німецької і перекладається як “служба за викликом”. І є слова на зразок Werkshöfe – подвір’я фабрик.

Топ 50 слів виглядає так, і складає 28% всіх слів загалом:

der 58761447 sich 9169933
und 49084873 wurde 9114619
die 44536463 CET 8614461
in 35684744 an 8385637
von 24448221 er 7835324
ist 20614114 dass 7550955
den 19454023 du 7435099
nicht 17519638 bei 7420172
das 17302844 Diskussion 7237855
zu 16167971 aus 7065523
mit 15906145 Artikel 6967243
im 15167140 oder 6824420
des 14661593 werden 6508092
für 14016308 war 6449858
auf 13957013 nach 6426826
auch 12849476 wird 6117566
eine 11903977 aber 6052645
ein 11780352 am 6017703
Kategorie 11369651 sind 5953632
als 11167157 Der 5623930
dem 11124726 Das 5545595
CEST 11104741 einen 5465687
ich 10886406 noch 5409154
Die 10761776 wie 5293658
es 10204681 einer 5228368

До списку потрапили такі вікіпедійно специфічні слова як Kategorie (сторінки без категорій вважаються не комільфо), CEST і CET (центральноєвропейський літній час і центральноєвропейський час, в підписах в обговореннях).

Ну а що без сторінок обговорень? Проблемав тому, що при створенні об’єкту сторінки XMLDumpOldPageGenerator бере з дампа лише текст і заголовок, простір імен залишається не заповненим і за замовчуванням 0 (основний). Є ще поле isredirect так при спробі доступу до нього знову здійснюється запит. Тому, краще перейти на рівень нижче і використати XmlDump з pywikibot.xmlreader, він використовується так само, просто дає об’єкти не Page, а попростіші, які не вміють робити запити до вікіпедії і не мають методу save. Але нам його й не треба, правда?

Ось код який ігнорує перенаправлення і всі сторінки крім статтей:

"""Count word frequencies in wikipedia dump"""
import csv
from collections import Counter
from itertools import islice
import re
import sys

import mwparserfromhell
from pywikibot.xmlreader import XmlDump

def main():
    """Iterate over pages and count words"""
    if len(sys.argv) < 2:
        print('Please give file name of dump')
        return
    filename = sys.argv[1]

    pages = 0
    words = 0
    words_counts = Counter()
    print('Processing dump')

    for page in XmlDump(filename).parse():
        if (page.ns != '0') or page.isredirect:
            continue
        try:
            text = mwparserfromhell.parse(page.text).strip_code()
        except Exception as e:
            print(page.title, e)
            continue

        text = text.replace('\u0301', '') # remove accents
        # Ukrainian: 

        # page_words = re.findall(
        #     r'[абвгґдеєжзиіїйклмнопрстуфхцчшщьюя'
        #     r'АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ’\'-]+',
        #     text
        # )
        
        # Any language:
        page_words = re.findall(r'\b[^\W\d]+\b', text)

        pages += 1
        words += len(page_words)
        words_counts.update(page_words)
        if pages % 123 == 0:
            print('\rPages: %d. Words: %d. Unique: %d. Processing: %s' % (
                pages, words, len(words_counts), (page.title + ' ' * 70)[:70],
            ), end='')

    print('Done. Writing csv')
    with open('common_words.csv', 'w', newline='') as csvfile:
        csvwriter = csv.writer(csvfile)
        for item in words_counts.most_common():
            csvwriter.writerow(item)

if __name__ == '__main__':
    main()

Він працює майже вдвічі швидше, 381 хвилину, бо обробляє лише 2,295,426 сторінок (обсяг німецької вікіпедії цього року). На цих сторінках є 1,074,446,116 слів (в середньому 468 на сторінку), з них різних – 12,002,417. (Виявляється є аж 6 мільйонів всяких слів які вживаються на всіляких службових сторінках німецької вікіпедії, і яких нема в статтях).

Якщо ж взяти українські статті, то на них треба ще менше часу – 131 хвилину (забув уточнити що в мене SSD), їх є 923238 (скоро мільйон!), слів 238263126 (в середньому 258 на сторінку, треба доповнювати 😉 ). З них різних – 4,571,418. Отак, в мене тепер є частотний словник української на 4.5 мільйони слів. І німецької на 12 мільйонів.

Хоча не спішіть з висновками що українська мова бідніша, бо мої методи потребують вдосконалення. По перше, так як Morgen (ранок) і morgen (завтра) – різні слова, то я не приводив букви в німецькій до одного регістру. (Правда й в українській забув це зробити).

По друге, в німецькому словнику 350590 разів зустрічається слово “www”, бо я вважав словом будь-яку послідовність літер латинки, а в українській відфільтрував кирилицю. Слово youtube зустрічається 8375 разів, а значить є ризик знайти якесь рідкісне слово на зразок “fCn8zs912OE”. 🙂

На WordPress глючить додавання картинок, тому нате вам відео:

А, і ось топ-10 української вікіпедії:

в,4551982
на,3730686
і,3475086
у,3353796
з,3053407
-,2695783
Категорія,2417267
та,2350573
до,1815429
року,1553492

Частота “року” наводить на думку що в українській вікіпедії якийсь перекос на історичні методи викладу. 🙂

Кілька рядків коду що підвищують продуктивність в рази

«Не потоком шумних і галасливих фраз, а тихою, невтомною працею любіть Україну!»

Це клікбейтний заголовок, але тут ми дійсно за пару хвилин напишемо розширення до браузера, щось на зразок resquetime тільки просте як одноклітинні. Назвемо його наприклад higher power, бо воно працюватиме як підстраховка для нашої власної сили волі.

Створюємо директорію проекту

bunyk@bunyk-thinkpad:~/projects$ mkdir higher_power
bunyk@bunyk-thinkpad:~/projects$ cd higher_power/

А в ній файл manifest.json з наступним вмістом:

{
  "manifest_version": 2,
  "name": "Higher power",
  "version": "1.0",
 
  "description": "Helps you to avoid temptations",
 
  "icons": {
  },
 
  "content_scripts": [
    {
      "matches": ["*://*.facebook.com/*"],
      "js": ["power.js"],
      "run_at": "document_start"
    }
  ]
}

Версії і назва – обов’язкові поля, опис та icons – ні, але корисні, бо відображатиметься в списку додатків, а content_scripts описує який код завантажуавти при відкриванні певної адреси.

“run_at” каже запускати код ще до того як сторінка завантажиться, без цієї опції браузер пару секунд рендерить стрічку фейсбука, а тоді вже наш аддон починає щось робити.

Створимо цей код, в згаданому в маніфесті файлі power.js, нариклад так:

window.location.href="https://bunyk.wordpress.com/";

Замість bunyk.wordpress.com можна вписати https://www.udacity.com/, https://www.edx.org/, https://github.com/ чи щось таке.

Тепер відкриваємо в Firefox сторінку “about:debugging” (для інших браузерів самі з’ясуйте що і напишіть в коментарі будь ласка), натискаємо кнопку “Load Temporary Add-on…”, вибираємо будь-який файл з директорії нашого проекту, і насолоджуємось результатом.

Посилання

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension

Як створити блог з Hugo

Hugo – це такий генератор статичних сайтів. Статичні сайти – це сайти що складаються з набору фіксованих сторінок і не генеруються з шаблонів і запитів до бази даних при кожному завантаженні. Це з одного боку менш зручно бо немає можливості наприклад опублікувати щось автоматично встановивши час публікації, а з іншого боку – менш вимогливо до ресурсів, і краще з точки зору комп’ютерної безпеки. Крім того, wordpress.com зі своїми оновленнями інтерфейсу починає трохи дратувати. Хочеться markdown, свого javascript і стилів. А ще він не підсвічує синтаксис go. 🙂 Але ця стаття публікується на WordPress, яка іронія… Тому що я ще не вирішив що публікуватиму там.

До цього, мій статичний сайт на github генерувався самописним скриптом на python, який перетворював шаблони Mako в HTML, дозволяв вставляти javascript разом з залежностями, і так як я коли це придумував не знав ні про який node.js з npm (точніше знав, але не дуже цікавився), то залежності в мене описувались не в package.json, а в external_assets.py, і збирав їх не webpack чи gulp, чи browserify чи bower, а requirejs.py.

Юний я і мій велосипед.

Але це я відхиляюсь від теми. Мова про те що велосипеди треба не винаходити, треба брати і їздити. Тому поїхали!

Якщо у вас встановлене go, я зараз розкажу як встановити hugo, інакше читайте інструкцію для своєї системи.

Виконуємо:

go get github.com/magefile/mage
go get -d github.com/gohugoio/hugo
cd ${GOPATH:-$HOME/go}/src/github.com/gohugoio/hugo
mage vendor
HUGO_BUILD_TAGS=extended mage install

HUGO_BUILD_TAGS=extended потребує встановлених gcc, та g++, щоб скомпілювати libsass. Я сам писатиму просте CSS без всяких там SASS, але не знаю чи якимось темам його не треба буде, тому на всяк випадок скомпілював з його підтримкою.

Перевіряємо версію:

$ hugo version
Hugo Static Site Generator v0.48-DEV-FFF13253/extended linux/amd64 BuildDate: 2018-08-22T22:49:10+0300

Свіже щойно збілджене.

$ cd ~/projects # чи де ви там свій код тримаєте
$ hugo new site blog # створюємо новий сайт що називається "blog".
$ cd blog

Додаємо тему. Вибрати можна тут.

git init # можна звісно й без git її скачати, але так зручніше
git submodule add https://github.com/Vimux/Mainroad.git themes/mainroad
echo 'theme = "mainroad"' >> config.toml

Додаємо якусь публікацію:

hugo new posts/my-first-post.md

Запускаємо сервер

hugo server -D

Можна запускати без -D, але тоді щоб побачити публікацію, треба забрати з неї рядок “draft: true” (його рано чи пізно варто буде забрати, а то нащо сайт, якщо на ньому нема закінчених публікацій?). Всі публікації знаходяться в директорії content (ну а потім posts/my-first-post.md).

В браузері дивимось як виглядає наш сайт. В темі Mainroad ви публікацій спершу не побачите, бо вона їх чомусь за замовчуванням шукає в директорії content/post, хоча документація Hugo каже створювати в posts. Це не страшно, в файлі config.toml додаємо ще секцію “Params” з наступним вмістом:

[Params]
  postSections = ["posts"] # the section pages to show on home page and the "Recent articles" widget

Було б добре ще додати якусь сторінку about і т.п. Це теж просто:

hugo new about/_index.md

Редагуємо content/about/_index.md і додаємо вгорі menu: main, інакше посилання на нашу сторінку не буде в меню. Тепер є меню з одного пункта, але нема посилання назад на головну зі списком публікацій.

Щоб виправити це – додайте наступне в конфіг:

[menu]

  [[menu.main]]
    identifier = "home"
    name = "Home"
    url = "/"
    weight = -110

Можна посилання ще кудись, додавши ще секцію:

  [[menu.main]]
    identifier = "bunyk"
    name = "Блог одного кібера"
    url = "https://bunyk.wordpress.com/"
    weight = 100

“weight” (вага) – це число за яким сортуються пункти меню в зростаючому порядку.

Ще, мені не подобається що в цій темі посилання червоні і не підкреслені. Я люблю щоб були сині з підкресленням. Для цього створюємо файл static/style.css, який перевантажуватиме стилі нашої теми:

.content a {
	color: blue;
	text-decoration: underline;
}

Правда його існування ще нічого не міняє, бо тема про нього не в курсі. Аби була в курсі, треба скопіювати файл теми themes/mainroad/layouts/_default/baseof.html в layouts/_default/baseof.html, і додати там після рядка:

	<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">

рядок:

	<link rel="stylesheet" href="{{ "style.css" | relURL }}">

Тепер досить бавитись на localhost, давайте опублікуємо все десь в інтернеті. Я спробую на Github Pages, бо там в мене вже був велосипед.

Виявляється, це майже елементарно. Якщо у вас нема репозиторію що називається .github.io – створіть. Там буде публікуватись відрендерений сайт. Тоді видаліть з проекту директорію public (потім hugo її перестворить), і створіть на її місці підмодуль що вказуватиме на репозиторій сайту:

git submodule add -b master git@github.com:<USERNAME>/<USERNAME>.github.io.git public

Підмодуль – це таке посилання на конкретний комміт в іншому git репозиторії. Щоб його оновити – перебудовуємо сайт (команда hugo без параметрів), переходимо в public, комітимо і пушимо. Готово. Можна автоматизувати останні дії таким скриптом:

#!/bin/bash

echo -e "\033[0;32mDeploying updates to GitHub...\033[0m"

hugo 

cd public

git add .
msg="rebuilding site `date`"
if [ $# -eq 1 ]
  then msg="$1"
fi
git commit -m "$msg"

git push origin master

cd ..

Скільки символів потрібно щоб написати вікіпедію?

Не так важливо скільки там людей говоритиме українською коли настане технологічна сингулярність. Важлива сумарна обчислювальна потужність інтелекту що володіє українською. 😉 І взагалі варто опановувати хоч якісь основи навчання машин – це професія в якій роботи ще не скоро замінять людей. Щоб навчити машину мови – їй треба багааатезно прикладів. І найбільший шмат української мови який можна легко згодувати машині – вікіпедія. Тому сьогодні ми спробуємо з’ясувати що потрібно щоб отримати цей масив тексту, і порахувати на ньому якусь простеньку статистику, для якої не треба бази даних а вистачить оперативної пам’яті.

Перше що нам потрібно – копія бази даних вікіпедії. Тому що вікіпедія містить більше чверті мільйона статтей, і навіть якщо ми робитимемо по запиту на секунду, що вікіпедія не схвалює для всяких там приватних павуків, то складання індексу займе в нас (750000 сек)/ 3600 / 24 = 8.68 діб > тижня. Тому заходимо на
https://dumps.wikimedia.org/ , вибираємо дамп який більше подобається, наприклад останній дамп що містить статті (без сторінок обговорень) української вікіпедії і ставимо на скачування.

А поки воно скачується підготуємось його розпаковувати. Ми скачуємо заархівований XML, який при розпаковуванні займає щось біля 5GB. Всередині є багато тисяч елементів , кожен з яких містить деталі про сторінку. Ось код на Go який містить функцію Read що розархівовує і водночас парсить XML, та повертає канал в який кидає сторінку за сторінкою, а в головній функції ітерується по всіх сторінках і підраховує кількість символів в їх тексті. В кінці виводить статистику:

Можна запускати його як go run main.go *.xml.bz2, а можна зробити go build і отримати скомпільований код, який вже запускати. Думаю за швидкістю вони співмірні.

Дізнаємось що українська вікіпедія станом на 3 березня (бо такий в мене дамп) складається з 4 138 529 133 символів, серед яких різних лише 24 190.

Різних символів не так вже й багато, для порівняння остання версія юнікоду має 136 755, але все одно забагато як на мене. Наприклад якщо захочеться скласти таблицю як часто який символ йде після даного (біграми), то в ній буде 585 156 100 клітинок. Це треба буде пару гігабайт оперативки аби порахувати лише диграми. Сторінок там приблизно 1 600 000 (а не 750 000), тому що крім статтей є ще шаблони, категорії, і тому подібні речі. Тому я думаю треба відкинути неважливі символи, наприклад взявши за поріг що символ має з’являтись у вікіпедії хоча б 16 000 разів (якщо менше, то шанс побачити його на випадково взятій сторінці менше 1%). А таких лише 229, і ось їхній номер в рейтингу, написання і кількість використань у вікіпедії:

229) 市: 17347
228) λ: 18004
227) ç: 18915
226) ε: 19037
225) č: 19328
224) ​: 19388
223) ñ: 19431
222) ل: 19786
221) ў: 20711
220) @: 21351
219) ą: 21381
218) É: 22425
217) ś: 22544
216) š: 22885
215) ′: 23578
214) ș: 23592
213) ρ: 23834
212) ±: 24008
211) ς: 24200
210) τ: 24465
209) ι: 25389
208) à: 25608
207) ę: 25616
206) ν: 25716
205) ń: 25991
204) ”: 29662
203) ا: 32288
202) ο: 35475
201) ‎: 36211
200) „: 38162
199) →: 38323
198) í: 40358
197) ş: 42223
196) α: 43669
195) †: 45094
194) Ь: 46660
193) è: 50851
192) ö: 50927
191) “: 51455
190) ä: 51800
189) −: 52985
188) $: 55963
187) ё: 55973
186) ²: 58338
185) ţ: 60505
184) ×: 61094
183) ă: 63880
182) ъ: 64358
181) á: 65593
180) ^: 68237
179) ł: 69450
178) Э: 69917
177) Ї: 72349
176) ü: 74505
175) â: 86866
174) ½: 92706
173) э: 97009
172) …: 98257
171) ~: 104201
170) ó: 113417
169) °: 138459
168) Щ: 144944
167) Ґ: 153541
166) ·: 170010
165) ’: 172548
164) é: 308459
163) №: 308648
162) И: 309865
161) Й: 333321
160) •: 349671
159) \: 352804
158) ґ: 364613
157) ́: 389166
156) Z: 408490
155) +: 522576
154)  : 555014
153) Ю: 573682
152) q: 574906
151) ы: 604142
150) : 630305
149) Ж: 690154
148) Y: 723229
147) X: 749583
146) Є: 765597
145) Q: 818419
144) Я: 930365
143) –: 942145
142) J: 961000
141) ?: 1046419
140) Ц: 1098377
139) W: 1185842
138) Е: 1540741
137) O: 1628778
136) Х: 1696070
135) Ш: 1724585
134) K: 1802117
133) V: 1805294
132) H: 1816527
131) Ч: 1855640
130) j: 1864115
129) z: 2061416
128) U: 2194056
127) G: 2744752
126) N: 2852071
125) M: 3061885
124) #: 3154223
123) L: 3181356
122) !: 3189165
121) B: 3369071
120) З: 3437745
119) x: 3452972
118) F: 3520991
117) T: 3583564
116) %: 3642707
115) І: 3655626
114) P: 3725526
113) E: 3972385
112) »: 3977385
111) «: 3980397
110) Ф: 4202078
109) I: 4388726
108) R: 4396989
107) У: 4451260
106) О: 4554868
105) Б: 4662681
104) C: 4700683
103) щ: 4747530
102) Т: 4828013
101) Р: 5285196
100) Д: 5297868
99) A: 5304565
98) Н: 5308271
97) D: 5412754
96) Л: 5463510
95) v: 5601059
94) S: 5771420
93) —: 5876110
92) Г: 6196594
91) *: 6308184
90) _: 6536077
89) М: 6714179
88) є: 6725025
87) k: 6994569
86) &: 7431439
85) А: 8011695
84) В: 8034456
83) ф: 8301528
82) y: 8544168
81) ;: 9651561
80) С: 9778231
79) 7: 9836255
78) ш: 10032005
77) ю: 10424706
76) П: 10504497
75) w: 10609713
74) (: 10822363
73) ): 10861049
72) 6: 10922925
71) К: 11311101
70) 8: 11392131
69) “: 11480435
68) 4: 11994852
67) 5: 12855281
66) 3: 13149018
65) ж: 13461373
64) >: 14339625
63) <: 14353972
62) ї: 14915551
61) h: 15043805
60) m: 15056183
59) х: 16421470
58) f: 16484180
57) u: 17314042
56) g: 17753361
55) b: 18992562
54) 9: 19917121
53) :: 20761022
52) ц: 21063193
51) ч: 21089655
50) d: 21132407
49) p: 22184976
48) /: 22493164
47) c: 22688474
46) {: 23253440
45) }: 23256779
44) ,: 24259060
43) 2: 25895208
42) б: 27028220
41) ': 27719818
40) -: 29433960
39) й: 29504263
38) г: 32647216
37) з: 34131397
36) 0: 34211416
35) l: 35713324
34) .: 36224151
33) s: 36570492
32) o: 36864174
31) 1: 37256409
30) ь: 37873838
29) i: 37914159
28) я: 40403600
27) n: 41935115
26) r: 43452098
25) п: 45627724
24) a: 47339118
23) t: 49644507
22) м: 51192263
21) =: 52585455
20) д: 54742090
19) у: 56282864
18) e: 60511715
17)
: 68855138
16) к: 77929326
15) л: 78327225
14) [: 81918527
13) ]: 81923177
12) с: 85389668
11) в: 88260931
10) |: 90345542
9) т: 99966314
8) е: 100782938
7) и: 110643874
6) р: 118651169
5) і: 130028472
4) н: 154575149
3) о: 178231842
2) а: 180553597
1) : 506900824

Цих 229 символів використовуються сумарно 4 136 342 085 разів, а це складає 99.95% всіх символів вікіпедії. Тому рештою думаю дійсно можна знехтувати.

Простеньке Go API з JWT авторизацією

Щоб щось зрозуміти іноді легше писати ніж читати (В мене була потреба зрозуміти як працює middleware jwt авторизації, і я цей код читати не міг. Довелось для розминки написати аналогічне, трохи помогло).

Якщо вам цікаво що таке JWT і для чого, то в двох словах – це можливість видати комусь право доступу до чогось без бази даних де б писало що ми йому таке право давали. Тобто сервер на якому користувач авторизується, і сервер до якого він отримує доступ – це можуть бути два абсолютно окремі сервери, які не те що не мають спільної бази даних, вони навіть не спілкуються мережею. Головне – правильні ключі.

Напишемо наступне супер просте API:
POST /login {user: “”, password: “”} – віддає нам JWT токен для дозволу запису
GET / – віддає нам список записів
POST / – з заголовком “Authorization: Bearer ” дозволяє додати новий запис до списку, якщо ми авторизовані.

Для початку зробимо все без авторизації:

package main
 
import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)
 
func main() {
    initDB()
     
    http.HandleFunc("/", handler)  // За головну сторінку відповідатиме функція handler
    fmt.Println("Listening at 8080")
    http.ListenAndServe(":8080", nil) // Запускаємо сервер на якомусь порті
}
 
// "база даних"
var log []string
 
// Заповнюємо "базу" якимись даними
func initDB() {
    log = make([]string, 0)
    log = append(log, "Hello")
    log = append(log, "World")
}
 
func handler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        data, err := json.Marshal(log) // серіалізуємо базу в JSON
        if errorHandler(w, err, http.StatusInternalServerError) { // якщо була помилка
            return
        }
        w.Write(data) 
    } else if r.Method == "POST" {
        bodybytes, err := ioutil.ReadAll(r.Body) // Читаємо все що передано
        if errorHandler(w, err, http.StatusInternalServerError) {
            return
        }
        log = append(log, string(bodybytes)) // і додаємо як текст в базу даних
        w.Write(bodybytes) // і повертаємо що додали
    } else { // невідомий метод
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
    }
}
 
// Функція яка якщо отримує помилку пише у відповідь текст помилки з кодом, і повертає true,
// а якщо не було - просто повертає false.
func errorHandler(w http.ResponseWriter, err error, code int) bool {
    if err == nil {
        return false
    }
    fmt.Println(err)
    msg, _ := json.Marshal(map[string]string{
        "error": err.Error(),
    })
    w.Write(msg)
    return true
}

Запишемо це в файл наприклад main.go і запустимо go run main.go. Тепер можна перевірити все командами:

curl http://localhost:8080/ # Дає ["Hello","World"]
curl -X POST http://localhost:8080/ -d "it works!" # Дає it works!
curl http://localhost:8080/ # Дає ["Hello","World","it works!"]

Якщо працює – пора генерувати ключі. Бо ми ж не хочемо щоб будь-хто міг писати в нашу “базу даних”. Ключі в асиметричній криптографії завжди генеруються парами, один публічний, один приватний.

openssl genrsa -out key.rsa 1024
openssl rsa -in key.rsa -pubout > key.rsa.pub

Перевірте щоб файл key.rsa починався з рядка “—–BEGIN RSA PRIVATE KEY—–“, а key.rsa.pub – з “—–BEGIN PUBLIC KEY—–“. І не переплутайте, а то логін не буде безпечним і взагалі довго дебажити доведеться.

Приватний ключ використовується для підписування документа, тобто документ зашифровується цим ключем і додається до відкритого як підпис. Документом буде JSON Web Token, і він буде містити зрозумілі комп’ютеру твердження на зразок “власник цього токена має право записувати дані на сервер до такого-то числа, підписано сервером”. Токен буде видаватись якщо ми передамо правильні логін і пароль на “/login”. Можна вважати приватний ключ печаткою, а публічний – зразком печатки для порівняння.

Щоб підписувати токени ключами нам знадобиться бібліотека “jwt-go”. Встановлюється командою go get github.com/dgrijalva/jwt-go. Тепер нам треба трохи модифікувати функцію main() і додати бібліотек:

import ( // крім вищенаписаного додати ще ці, знадобиться:
	"crypto/rsa"
	"github.com/dgrijalva/jwt-go"
	"time"
)

func main() {
	initDB()
	err := loadKeys() // Завантаження ключів
	if err != nil {
		fmt.Println(err)
		return
	}
	
	http.HandleFunc("/login", loginHandler) // тут будемо видавати токен
	http.HandleFunc("/", handler) // а тут будемо іноді перевіряти

	fmt.Println("Listening at 8080")
	http.ListenAndServe(":8080", nil)
}

// В цих змінних зберігатимемо ключі
var PublicKey *rsa.PublicKey
var PrivateKey *rsa.PrivateKey

// Ця функція просто прочитає і розпарсить ключі у змінні вище, нічого цікавого
func loadKeys() error {
	pk, err := ioutil.ReadFile("./key.rsa")
	if err != nil {
		return err
	}
	PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(pk)
	if err != nil {
		return err
	}

	pk, err = ioutil.ReadFile("./key.rsa.pub")
	if err != nil {
		return err
	}
	PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(pk)
	return err
}

// Структура в яку ми розпакуємо запит до /login
type UserCredentials struct {
	Login    string `json:"login"`
	Password string `json:"password"`
}

// Якщо отримує запит з правильними логіном і паролем повертає нам
// підписаний токен для доступу до запису в "БД"
func loginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
		return
	}
	var user UserCredentials
	var err error
	bodybytes, err := ioutil.ReadAll(r.Body)
	err = json.Unmarshal(bodybytes, &user)
	if errorHandler(w, err, http.StatusUnprocessableEntity) {
		return
	}
	if (user.Login != "LOGIN") || (user.Password != "PASSWORD") {
		errorHandler(w, fmt.Errorf("Bad credentials"), http.StatusForbidden)
		return
	}

	// якщо всі перевірки пройдено - згенерувати токен
	tokenString, err := getJWT()
	if errorHandler(w, err, http.StatusInternalServerError) {
		return
	}
	msg, _ := json.Marshal(map[string]string{
		"token": tokenString,
	})
	w.Write(msg)
}

// Функція що повертає нам текст токена, підписаний приватним ключем
func getJWT() (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
		// тут записуємо дозвіл на post (взагалі, можна що завгодно записувати)
		"allow": "post",
		// пишемо що токен дійсний пів години
		"exp": time.Now().Add(time.Minute * 30).Unix(),
	})
	return token.SignedString(PrivateKey)
}

Пробуємо дістати токен:

curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "PASSWORD"}'
# Дає {"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00"}
curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "WRONG"}' 
# Дає {"error":"Bad credentials"}

Бачимо що токен – це три base64 стрічки записані через крапку. Є веб-сервіс що дозволяє розкодувати і подивитись що там. Перша – заголовок, описує формат токена:

{
  "alg": "RS256",
  "typ": "JWT"
}

Тіло – описує власне якісь твердження, в нашому випадку те що ми дозволяємо деякий час робити POST запити:

{
  "allow": "post",
  "exp": 1512941461
}

Тепер треба оновити основний handler, аби він перевіряв наявність і правильність токена.

func handler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		data, err := json.Marshal(log)
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		w.Write(data)
	} else if r.Method == "POST" {
		// Цього разу перед тим як робити POST
		allow, err := isPostAllowed(r)  // перевіряємо чи можна
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		if !allow { // І якщо не можна - повертаємо помилку
			errorHandler(w, fmt.Errorf("Access denied"), http.StatusForbidden)
		}
		bodybytes, err := ioutil.ReadAll(r.Body)
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		log = append(log, string(bodybytes))
		w.Write(bodybytes)
	} else {
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
	}
}

// Перевірка дозволів
func isPostAllowed(r *http.Request) (bool, error) {
	bearer := r.Header.Get("Authorization") // Отримуємо заголовок Authorization
	prefixLen := len("Bearer ")
	if len(bearer) <= prefixLen {
		return false, fmt.Errorf("Authorization header is too short")
	}
	// Пробуємо парсити токен. Два аргументи - токен і функція що повертає потрібний ключ
	token, err := jwt.Parse(bearer[prefixLen:], func(token *jwt.Token) (interface{}, error) {
		return PublicKey, nil
	})
	if err != nil {
		return false, err
	}
	// Якщо вийшло розпарсити, токен валідний, то дістаємо твердження
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		allow, ok := claims["allow"].(string) // дивимось чи написано щось про "allow"
		if ok && strings.Contains(allow, "post") { // і чи є там "post"
			return true, nil
		}
		return false, fmt.Errorf("Token does not have claim to allow this action")
	} else {
		return false, fmt.Errorf("Token is invalid")
	}
	return true, nil
}

Тепер попробуємо щось запостити без токена і з ним:

curl -X POST http://localhost:8080/ -d "it works!"
# Дає {"error":"Authorization header is too short"}
curl -X POST http://localhost:8080/ -d "it works!" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00"
# Дає it works

Працює! Отож, бачимо що JWT це дуже навіть просто, якщо не рахувати заморочок з дивними способами виклику функцій бібліотеки – виписуємо довідку що щось можна ставимо на неї печатку, а в іншому місці це перевіряємо.


Filed under: Кодерство, Павутина Tagged: go

Як швидко розпочати писати SPA на AngularJS (1)

Тому що чим швидше ми зробимо щось що зможемо помацати поклацати – тим менше сили волі буде треба для підтримки мотивації. (Так, я знаю що писати таку публікацію два роки – то задовго, але краще пізно ніж ніколи. Сподіваюсь що хоча б тому хто буде підтримувати проекти на Angular 1 (а я не впевнений що їх багато мігрує на новіші версії) це знадобиться).

Варто мати встановленим NodeJS. Він має менеджер пакетів npm. І з його допомогою ми скачаємо всі необхідні бібліотеки. Ми ж перестали шукати софт на сайтах ще коли почали користуватись менеджерами пакетів в Linux, те ж саме ми робимо коли нам треба бібліотека для python, то чим розробка для браузерів гірша?

Тут я був написав кілька абзаців про те як за допомогою npm поставити bower (інший менеджер пакетів), але це трохи збочення, бо npm нас може й сам задовольнити. Тому поки що обійдемось. Let the hacking begin.

Створюємо порожню директорію для нашого проекту, і в ній виконуємо:

npm init

На всі питання даємо стандартну відповідь. Це створить нам файл package.json куди прописуватимуться залежності нашого проекту (встановлені бібліотеки), і директорію для зберігання коду цих бібліотек.g

Поставимо наприклад Angular, і одну бібліотеку для нього, angular-route, яка допомже нам писати односторінковий застосунок:

npm install angular --save
npm install angular-route --save

--save просить окрім того що скачати код в node_modules/, також прописати нову залежність в package.json.

Тепер можемо закинути якийсь код в index.html і полюбуватись нашою статичною сторінкою. Наприклад такий (я писатиму календар, а ви можете все що захочете, принципи взагалі однакові):

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Календар на 2016-тий рік</title>
        <link rel="stylesheet" href="app/style.css" />
    </head>
    <body ng-app="main">
        <nav></nav>
        <div id="content" ng-view></div>

        <script src="node_modules/angular/angular.js"></script>
        <script src="node_modules/angular-route/angular-route.js"></script>
        <script src="app/index.js"></script>
    </body>
</html>

Сторінка порожня, але то нічого, ми її динамічно наповнимо календарями за будь-який рік і місяць. Для цього треба створити файл app/index.js. В цьому файлі помістимо створення модуля main, який буде підтягувати інші модулі як залежності.

Модуль створюється викликом angular.module(name, dependencies_list), де name – стрічка що містить ім’я модуля за яким до нього звертатимемось при потребі (як от в html вище, ще ми згадуємо модуль "main" в атрибуті ng-app), а dependencies_list – модулі які треба мати перед тим як створити наш.

Далі метод config об’єкта модуля дозволить описати його ініціалізацію. Йому передається список залежностей які треба впровадити (inject dependency), а в кінці нього функція яка ці всі залежності використовує. Впровадження залежностей це не так страшно як звучить, це просто означає що angular сам за іменем знайде або створить об’єкт який нам треба, і передасть його як параметр.

Ініціалізуємо все так:

var app = angular.module('main', ['ngRoute']);

app.config(function($routeProvider) {
    $routeProvider
        .when('/', {
            template: '<h1>Домашня</h1><p>Покажемо тут календар</p>',
        })
        .when('/about', {
            template: '<h1>Про проект</h1><p>Тут буде пояснення як все працює</p>',
        })
        .otherwise({
            template: '<h1>Не знайдено</h1>' + 
                '<p>Сторінку "{{vm.currentPage}}" не знайдено</p>' +
                '<button ng-click="vm.goBack()">Назад</button>',
            controllerAs: 'vm',
            controller: function($location) {
                var vm = this;
                vm.currentPage = $location.path();

                vm.goBack = function() {
                    window.history.back();
                };
            },
        });
});

$routeProvider.when('/'), каже нашому роутеру (системі яка пов’язує адреси та вміст сторінки), прив’язати певні адреси до певних сторінок і контролерів до тих сторінок. Вони не цікаві, бо містять лише параметр template – що показати. $routeProvider.otherwise(), каже що показувати якщо ми перейдемо на якусь невідому адресу. Він містить також параметри controller, який задає функцію яка заповнить свій контекст this всім чим захоче, і це все буде доступним в шаблоні під іменем яке ми задаємо в controllerAs. currentPage містить url хеша сторінки яка відкрита в браузері, функція goBack поверне користувача на один крок назад в історії переходів.

Про те як це все прив’язується до шаблону – читайте в коротенькому вступі до AngularJS.

Тепер, якщо ми оновимо нашу сторінку в браузері, її адреса закінчуватиметься так: /index.html#!/. Все що йде після символа “дієз” (#) називається хешем, і це частина адреси, зміна якої не викликає перезавантаження сторінки. Хешем маршрутизуються односторінкові застосунки, бо якщо URL перед ним не змінюється (а він не змінюється, браузер не відішле новий запит). Знак оклику – то SEO від Google, не зважайте.

Ми можемо змінити сторінку на /index.html#!/about або на якусь іншу. Але з клавіатури це робити важко (особливо якщо на телефоні, давайте додамо навігацію.

Можна додати до index.html в блок navдва посилання, і побачити як за різними адресами показують різний вміст:

        <nav>
            <a href="#!/">🏠</a>
            <a href="#!/about">Про проект</a>
        </nav>

Ось CSS аби ті посилання виглядали гарно (можна покласти його в app/style.css, ми ж його вже підвантажуємо в index.html):

* {
    margin: 0;
    padding: 0;
}
#content {
    padding: 50px;
}

nav {
    min-height: 50px;
    border-bottom: 1px solid black;
    width: 100%;
    position: relative;
}

nav div {
    position: absolute;
    bottom: 0;
    left: 0;
}

nav a {
    height: 100%;
    padding: 5px;
    display: block;
    float: left;
}

nav a.active {
    text-decoration: none;
    font-weight: bold;
    color: black;

    border: 1px solid black;
    border-radius: 5px 5px 0 0;

    border-bottom: 1px solid white; /* витирає нижню границю елемента nav */
    margin-bottom: -1px;            /* треба перевіститись на 1px вниз щоб її перетерти */

}

Воно ще не до кінця гарно, але скоро буде, ще трохи покращень. Наш код якось не дуже гарно виглядає, давайте переформатуємо трішки. Для початку, створимо директорію app/templates, і замінимо елемент nav таким:

        
    <nav ng-include="'app/templates/navigation.html'"></nav>

Ну а в navigation.html покладемо попередній вміст елемента nav:

<div>
    <a href="#!/" class="active">🏠</a>
    <a href="#!/about">Про проект</a>
</div>

Друга проблема – ми описуємо вміст сторінок прямо в головному модулі. Це з одного боку добре, бо всі сторінки завантажуються зразу, але з іншого боку – не добре, бо в JavaScript нема багаторядкових стрічок, та й підсвітка html синтаксису, яка допомагає швидше побачити помилки, всередині стрічок не працює. Щоб виправити цю проблему, заміюємо параметр template в обох when на

                templateUrl: 'app/templates/home.html'
                ...
                templateUrl: 'app/templates/about.html'

І переносимо вміст який треба відобразити у вказані файли. Ок, виглядає супер, може нарешті геренуватимемо календар динамічно?

Для цього нам треба функцію яка його малюватиме. Колись я написав модуль з такою функцією на CoffeScript: ось код. Щоб додати скомпільоване джерело цього модуля собі на сторінку, можна використати сервіс CDN http://rawgit.com/ :

        <script src="https://rawgit.com/bunyk/bunyk.github.com/master/dodecahedron/cal.js"></script>

Тепер, якщо в консолі ми напишемо

cal.text(2017, 5)

То отримаємо:

"Червень 2017
Пн Вт Ср Чт Пт Сб Нд
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30

Як вставити це на сторінку? Директива! Вставимо на нашу сторінку app/templates/home.html такий код:

<calendar
    ng-repeat="month in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
    year="2017"
    month="{{month}}"
></calendar>

Директива може бути елементом, може бути атрибутом елемента, може навіть прив’язуватись як клас, але найпопулярнішими є директиви як елементи. За замовчуванням, браузер не знає що то за елемент calendar, тому не відобразить нічого. Нам треба описати директиву. В фалі app/index.js додаємо:

app.directive('calendar', function() {
    return {
        restrict: 'E', // шукати цю директиву тільки серед елементів
        scope: { // Задання scope, означає що директива має ізольований простір імен
            // і не бачитиме змінні ззовні, окрім тих які ми протягнемо
            year: '@', // Собачка означає що це одностороння прив'язка з обчисленням виразу
            month: '@', // так, хтось може написати month="{{2+2}}" і ми отримаємо "4"
        },
        bindToController: true, // передати змінні зі scope в контролер
        // template, як і в when, задає вигляд нашої директиви
        template: '<pre class="month">{{vm.month_table}}</pre>',
        controllerAs: 'vm', // ім'я контрорела всередині директиви
        controller: function($scope) {
            var vm = this;
            $scope.$watch('vm.year', update); // слідкувати за зміною параметра year
            $scope.$watch('vm.month', update); // і за month, виконати update якщо щось зміниться
            function update() {
                // Тут ми реагуємо на зміну параметрів, зміною іншої змінної, на яку відреагує 
                // шаблон, і перемалює директиву
                vm.month_table = cal.text(vm.year, vm.month - 1);
            };
        },
    };
});

Для краси відображення місяців можна додати до стилів

.month {
    width: 180px;
    height: 130px;
    padding: 5px;
    margin: 10px;
    float: left;
    border: 1px solid #eee;
}

Тепер, було б добре якби календар був не лише на один рік. Це вже зовсім просто. Додаємо нову сторінку:

        .when('/:year/', {
            templateUrl: 'app/templates/home.html',
            controllerAs: 'vm',
            controller: function($routeParams) {
                var vm = this;
                vm.year = +$routeParams.year; // перетворюємо на int
            },
        })

Тут контролер просто отримує параметр з URL, і записує його собі, аби він був доступний в шаблоні. Шаблон теж перепишемо, аби він показував календар за вибраний рік, і додамо посилання на наступний і попередній роки:

<h1>Календар {{vm.year}}</h1>
<p>
    <a href="#!/{{vm.year - 1}}">Попередній ({{vm.year - 1}})</a>
    <a href="#!/{{vm.year + 1}}">Наступний ({{vm.year + 1}})</a>
</p>
<calendar
    ng-repeat="month in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
    year="{{vm.year}}"
    month="{{month}}"
></calendar>

Що показувати на домашній сторінці? Давайте показувати поточний рік. Для цього можемо описати роут без шаблону, з одним лише контролером який робить перенаправлення:

        .when('/', {
            template: '',
            controller: function($location) {
                var redirect = '/' + (new Date()).getFullYear() + '/';
                $location.path(redirect);
            }
        })

І це все! Ми маємо календар на всі роки:

Вміст всіх файлів можна подивитись тут, я тільки замінив “/” на “_”, бо Gist не дозволяє таке в іменах файлів.

Якщо ви успішно пройшли цей вступ, маєте досвід програмування на JavaScript чи інших мовах, присилайте резюме за адресою roman@proofpilot.com, там ви потрібні. 😉


Filed under: Кодерство, Павутина Tagged: Angular, JavaScript

Як написати бота до Telegram?

Легко. 🙂 Давайте напишемо бота який перекладатиме нам всяке з німецької:

Приклад діалогу

Для цього нам треба поговорити з botFather-ом:

А зараз трохи не по темі цієї статті. Ось код який перетворює вікідані на словник, шукаючи всі сутності які мають мітки однією мовою, а потім показучи їх мітки іншою мовою, використовуючи хитрий запит SPARQL:

import json
import requests

def translate(from_lang, to_lang, word):
    '''
        Переклдає мітки елементів вікіданих з мови на мову. Повертає список варіантів перекладу
    '''
    res = sparql('''
        SELECT  ?ukLabel WHERE {
          ?item ?label "%s"@%s.
          ?item rdfs:label ?ukLabel filter(lang(?ukLabel) = "%s")
        } LIMIT 10
    ''' % (word, from_lang, to_lang))
    return list(map(
        lambda e: e['ukLabel']['value'],
        res['results']['bindings']
    ))

def sparql(query):
    ''' Отримує JSON дані запиту SPARQL до вікіданих '''
    res = requests.get(
        'https://query.wikidata.org/sparql',
        params={
            'query': query,
            'format': 'json'
        }
    )
    return json.loads(res.text)

А тепер повертаємось до теми телеграмного бота. Аби його написати треба поставити бібліотеку:

pip install pyTelegramBotAPI

Ось її Github: https://github.com/eternnoir/pyTelegramBotAPI

А далі – елементарно як писати консольну програму:

import telebot

TOKEN = '' # тут вставити те що BotFather сказав

bot = telebot.TeleBot(TOKEN)

@bot.message_handler(content_types=["text"]) # Якщо прийдуть нові повідомлення
def respond_to_message(message):
    translations = translate('de', 'uk', message.text) # Отримати переклади тексту повідомленя
    resp = '\n'.join(translations) if translations else 'На жаль, перекладу слова %s не знайдено' % message.text
    bot.send_message( # відправити назад
        message.chat.id, # в той самий чат з якого прийшло (можна напевне й в інший)
        resp
    )

if __name__ == '__main__':
     bot.polling(none_stop=True) # Запустити бота аби той сидів на лінії і слухав повідомлення.

Поки що все, бо й висипатись іноді треба. Пізніше нагадайте мені не забути написати більше про SPARQL, як поставити собі локальну mediawiki і розширення до неї, як логінити сторонні застосунки через OAuth, і як переписати інтерфейс вікіпедії на Vue.js. 🙂


Filed under: Кодерство, Павутина Tagged: вікіпедія, Python

Конспект Vue.js

Не варто припиняти вчити щось нове, правда? І писати – надійніший метод запам’ятати ніж просто читати, тому спробую повернути блог до життя.

CDN

Найпростіший спосіб яким ви можете почати використовувати Vue – це завантажити його на свою сторінку з CDN: https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js

Hello world!

Якщо у нас є такий HTML шаблон:

<div id="app">
  {{title}}
</div>

То мінімальний JavaScript який дозволяє його заповнити виглядає так:

var data = { // Модель - це просто будь-який об'єкт
    title: "Hello world!"
};
new Vue({
  el: "#app", // вибрати елемент за id
  data: data  // приєднати модель
});

Тепер, якщо в консолі браузера написати:

data.title = 'It works!'

То текст на сторінці зміниться автоматично. (І не треба ніякої мороки з дайджест-циклом через angular.element(e).scope().$apply() (Ангуляр-страждання, забийте)).

От так в’ю оновлюється коли змінюється модель. Як користувач може змінити модель?

Двостороннє з’єднання даних

Не змінюючи JavaScript додамо в наш HTML поле для вводу тексту (зверніть увагу на атрибут v-model):

<div id="app">
  <h2>{{title}}</h2>
  <input type="text" v-model="title" />
</div>

Все, тепер при зміні тексту в полі редагування, зміниться і заголовок.

Відображення масивів

Змінимо дані аби вони містили якийсь масив, наприклад з іменами атрибутів об’єкта Vue:

var data = {
    title: "Vue contains:",
    list: Object.keys(Vue)
};

Тепер, аби повторити якийсь елемент HTML для кожного елемента масиву, використовуємо атрибут v-for:

<div id="app">
    <h2>{{title}}</h2>
    <ul>
        <li v-for="key in list">{{key}}</li>
    </ul>
</div>

Оновивши сторінку в браузері побачимо щось таке:

Vue contains:

  • util
  • set
  • delete
  • nextTick

І знову ж таки, при зміні того списку, вміст сторінки оновлюється.

Класи і стилі

Добре, вміст елементів можна описати шаблоном в фігурних дужках, а як змінювати оформлення? Нехай ми хочемо показувати заголовок чи поле для його редагування залежно від того чи поставлена десь галочка. Нам потрібний заголовок, його редактор і перемикач:

<div id="app">
    <h2>{{title}}</h2>
    <input type="text" v-model="title">
    <label><input type="checkbox" v-model="edit">Edit</label>
</div>

Модель проста:

var data = {
    title: "Change me",
    edit: false
};

Залишилось приховати редактор коли edit == false і приховати заголовок коли він true.

Використовуємо атрибут v-bind:style, який приймає вираз що повертає об’єкт з властивостями CSS:

    <h2 v-bind:style="{display: edit ? 'none': 'block'}">{{title}}</h2>
    <input type="text" v-model="title"
        v-bind:style="{display: edit ? 'inline': 'none'}"
    />

Якщо ж у нас є клас для якого описаний стиль:

.hidden {
    display: none;
}

То можна використати v-bind:class який задає вираз що повертає об’єкт, ключами якого є назви класів, а булевими значеннями задається чи клас буде застосований до об’єкта. Також можна повернути масив з класами.

    <h2 v-bind:class="{hidden: edit}">{{title}}</h2>
    <input type="text" v-model="title" v-bind:class="{hidden: !edit}" />

Кнопки та інші події

Добре, а що якщо я хочу аби щось робилось при натисненні на кнопку? Наприклад те ж перемикання режиму редагування.

Спочатку треба оновити код створення нашого об’єкта Vue, передавши в його конфігурацію також поле methods. Це об’єкт який містить методи доступні з HTML. Методи мають доступ до моделі через змінну this. Код перемикача режиму редагування тривіальний:

new Vue({
    el: '#app',
    data: data,
    methods: {
        toggleEdit: function() {
            this.edit = !this.edit;
        }
    }
});

Тепер, щоб запускати цей метод, додамо справа від заголовка кнопку. Дія яка відбувається по кліку на елементі задається в атрибуті v-on:click:

<h2 v-bind:class="{hidden: edit}">
        {{title}}
        <button v-on:click="toggleEdit">Edit</button>
    </h2>

Залишається лише проблема того що при перемиканні в режим редагування кнопка пропадає разом із заголовком, бо вона в ньому. Але це й логічно, бо ми вже в режимі редагування, кнопка “Edit” нам не потрібна. Давайте вимкнемо редагування коли користувач натисне “Enter”. Знаєте як це зробити? v-on:keyup.enter

    <input type="text"
           v-model="title"
           v-bind:class="{hidden: !edit}"
           v-on:keyup.enter="toggleEdit"
     />

Ооо, дас іст фантастіш! Хіба ні? Але ще краще, це така частовживана директива, що замість v-on: можна писати @, і v-on:click="toggleEdit", можна написати @click="toggleEdit".

P.S. Нестандартні для HTML атрибути елементів як і в Angular називаються директивами, і я їх надалі так називатиму. У Vue-js починаються з префікса v-.

Обчислювані властивості

Давайте напишемо форму для вводу паролю з валідацією.

<div id="app">
    <input type="password"
           v-model="password"
           placeholder="Choose password"
    />
    <br />
    <input type="password"
           v-model="rePassword"
           placeholder="Repeat your password"
    />
    <p>{{formError}}</p>
</div>

В formError будемо класти повідомлення про те що паролі закороткі або не співпадають. Але як? Є ще одне поле куди можна складати функції, називається computed. Ці функції автоматично переобчислюватимуться коли змінюється модель, а в HTML виглядають як звичайні змінні. Тому валідація може виглядати так:

var data = {
    password: '',
    rePassword: '',
};
new Vue({
    el: '#app',
    data: data,
    computed: {
        formError: function() {
            if(this.password.length < 8) {
                return 'Password should have more than 8 characters';
            }
            if(this.password != this.rePassword)
                return 'Passwords did not match';
            return 'Ok';
        }
    }
});

Питання для самоконтролю

Чув що складати списки питань – ефективніший спосіб вчитися ніж просто читати.

  1. Які параметри приймає Vue і які їх функції?
  2. Що таке v-model і для чого воно?
  3. Що таке v-for і як воно використовується?
  4. Що таке v-bind:style?
  5. Що таке v-bind:class?
  6. Що таке v-bind:class?
  7. Який атрибут задає дію при клацанні по елементу?
  8. Де можна описувати методи які викликаються при різних подіях?
  9. Як викликати метод submitForm при натисненні в елементі клавіші Enter?
  10. Що позначає символ “@”?
  11. Що описують в полі computed</code?

Література

Конспект покриває сторінки 15 по 46 книжки (але тут приклади коду мої, текст теж)

  • Filipova, Olga (2016). Learning Vue.js 2: learn how to build amazing and complex reactive web applications easily with Vue.js. ISBN 978-1-78646-113-1.

Підтримати автора книжки можна тут: https://www.packtpub.com/web-development/learning-vuejs-2


Filed under: Кодерство, Павутина Tagged: JavaScript

Побудова “скриньок з вусами” львівських квартир що здаються на сьогодні

Я ще минулого року помітив що в питаннях про Python на StackOverflow обговорюють якісь панди. Це, як виявилось обгортка навколо matplotlib, numpy і подібних гарних речей. А ще, лазячи по своїх документах в Google знайшов скачану вже позаминулого року стіну групи пошуку нерухомості вконтакті. І так співпало що я і мій колега-аналітик зараз шукаємо квартиру у Львові. Я йому показав цей файл, і він загорівся бажанням проаналізувати ще якийсь сайт оголошень.

При всій повазі до lun.ua, але тут я прорекламую dom.ria.com. Передовсім, там є українська версія. А ще, можливість скачати результати пошуку як електронну таблицю, хоч і в xls форматі, і лише одну сторінку.

В python читати xls вміє бібліотека xlrd, тому треба доставити ще й її. Pandas взагалі має багато необов’язкових залежностей:

sudo pip3.5 install jupyter pandas xlrd matplotlib
jupyter notebook # дуже модний графічний інтерпретатор

Якщо все поставити як вище і запустити “jupyter”, то можна робити обчислення в отакому документі: https://github.com/bunyk/mypandas/blob/master/dom.ria/dom.ria.ipynb

І можна побудувати графік скринька з вусами:


От, недаремно я деякі лекції з АнДану все таки не проспав! Хоча, який висновок робити з цього графіка – не знаю. Знаю лише що половина квартир потрапляють всередину прямокутника.

А ось гістограми по цінах для однокімнатних і двокімнатних:

Однокімнатні

Однокімнатні

Двокімнатні

Двокімнатні

Який з цих гістограм робити висновок окрім того що квартир дешевших за 2000 грн (окрім викидів) не буває (а я зараз живу за 700 грн/міс, хоча це пів квартири) – теж не знаю. Може ви самі якийсь зробите. І так, до речі, я шукаю одно чи двокімнатну квартиру десь в другому або третьому квартилі цін в районі вулиці Липинського.


Filed under: Інструменти, Кодерство, Павутина Tagged: графіка, математика, Python

Зміни моделі, події і чистота функцій в Elm

Ця публікація містить ретельно закоментовану альтернативу TodoMVC на Elm. Правда щоб зрозуміти все одно спершу варто прочитати приклади Elm на вікіпедії і основи архітектури Elm програм (вона подібна до Redux якщо ви знаєте що це слово означає (бо я не знаю)).


Filed under: Кодерство, Павутина Tagged: elm, JavaScript