Tag Archives: Павутина

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

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

Перше що нам потрібно – копія бази даних вікіпедії. Тому що вікіпедія містить більше чверті мільйона статтей, і навіть якщо ми робитимемо по запиту на секунду, що вікіпедія не схвалює для всяких там приватних павуків, то складання індексу займе в нас (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

Упс, я зламав сайт ЦРУ

Точніше вони самі здається дев конфіг на прод залили. Але таке зі мною трапляється рідше ніж я переглядаю фільми про Джейсона Борна, тому зробив скріншот на пам’ять:

Selection_066


Filed under: Нещоденник, Павутина

Як написати свій букмарклет?

Букмарклет (bookmark) – це слово створене поєднанням слів bookmark (закладка) і applet (application (застосунок) + зменшувальний суфікс -let). Таким чином правильним перекладом bookmarklet було б “закластуночок” (чи “застокладочка”), але ви цього краще не кажіть Юрку Зеленому. Але я відхиляюсь. Отож, букмарклет, це маленька програма що міститься в закладці.

Як її туди помістили? Ну, виявляється браузери крім протоколів http, ftp, ітп, розуміють такий протокол як javascript. І коли отримують посилання з цим протоколом, при кліку по ньому не змінюють сторінку, а виконують його href як код JavaScript. Щоб побачити приклад простого букмарклету, створіть документ bookmarklet.htm, і помістіть в нього такий код:

<html>
<head><meta charset="utf-8" /></head>
<body>
<a href="javascript:
    alert('Привіт!')
">Мій букмарклет</a>
</body>
</html>

Якщо відкрити його в браузері, ви побачите чисту сторінку з єдиним посиланням Мій букмарклет, яке при кліку буде показувати повідомлення “Привіт!”. Якщо перетягнути це посилання на панель закладок в браузері, то тепер ви зможете в будь-який момент отримати привітання на будь-якій сторінці яку дивитесь, досить лише натиснути букмарклет.

Але це не дуже корисний букмарклет. Щоб він робив щось корисніше, досить замінити код в рядку 5, на якийсь корисніший.

Наприклад є такий сайт – lib.rus.ec. Вони хоч мають домен еквадору – але москалі, і коли відкрити їх сайт, то вискакує повідомлення, яке блокує доступ до сторінки, і просить грошей за чужі книжки. Я переконаний що в цьому випадку дозволяється використати принцип “грабуй награбоване”, і додати в букмарклет такий код:

 document.getElementsByClassName('lsp-overlay')[0].remove()

І тоді при кліку по букмарклету це вікно пропадатиме. Клас, правда? (Не забудьте забрати виклик alert() який ми написали там раніше)

Але давайте зробимо ще щось складніше і корисніше. Давайте зробимо букмарклет, який оформлює посилання на веб-сторінку в форматі вікіпедії cite web, і дає її нам в якомусь вікні. Це такий шаблон, який гарно оформлює посилання, але заповнювати його руками – ще та морока.

Для букмарклетів є кілька правил. Їх код все таки міститься всередині атрибуту елемента HTML, і повинен відповідати стандартам цієї мови. Наприклад всі подвійні лапки """ (ну ви зрозуміли), повинні бути заміненими HTML-кодом ". Крім того, перехід на новий рядок вважається еквівалентним пробілу, тому однорядкові коментарі можливі лише в кінці скрипта:

alert('Це вискочить');
// alert('Це не вискочить бо закоментовано');
alert('Це теж не вискочить бо коментар продовжується');

Ну і тому що ввесь скрипт здається браузеру написаним на одному рядку – бажано шанувати крапки з комою.

Якщо ми хочемо написати великий букмарклет, який можливо використовуватиме jQuery, та інші бібліотеки, нам захочеться їх якось підвантажувати. Але, віднедавна в адміністраторів серверів з’явилась нова можливість збільшувати безпеку вводячи обмеження на джерела з яких на сайт завантажуються ресурси – Content Security Policy, тому такі букмарклети можуть працювати не всюди. Можливо доведеться все переписати, мініфікувати і запакувати, але на всяк випадок, ось вам код підвантаження скрипта:

(function () {
    var script = document.createElement('script');
    script.src='https://gist.githubusercontent.com/bunyk/6ae97e5c3de490cfb4a1/raw/b7fcfde5c40314262893761418a6a25dd6ed0ce8/cite_web.js';
    document.body.appendChild(script);
}());

Подібним чином вже з того скрипта ми можемо підвантажити jQuery та наприклад CSS.

Я тут наковбасив трохи JavaScript, який можливо не на всіх сайтах працює, і не всі дані статті витягує, але на моєму блозі він дає щось подібне до цього:

cite_web

Щоб поставити собі такий, перенесіть оце посилання в закладки: “{{cite web|…“, а тоді в контекстному меню закладки натисніть “Властивості”, чи “Редагувати”, і замініть текст на

(function () {
    var script = document.createElement('script');
    script.src='https://gist.githubusercontent.com/bunyk/6ae97e5c3de490cfb4a1/raw/3102c3e285c6e24a9aed98d1bc7f0b7abeda20fc/cite_web.js';
    document.body.appendChild(script);
}());

Ось код:


Filed under: Кодерство, Павутина Tagged: Лайфхаки, вікіпедія, JavaScript

Логуючий фільтр для Angular expression

Вирази в Angular (те що в фігурних дужках і ngBind) – не зовсім те що Javascript. Так каже документація.

Context: JavaScript expressions are evaluated against the global window. In Angular, expressions are evaluated against a scope object.

А тому ми не можемо там використати console.log. Але можемо написати наприклад такі фільтри:

    angular
        .module('starter')
        .filter('log', function() {
            // do not change value, but log it in console
            return function(input) {
                console.log(input);
                return input;
            };
        })
        .filter('justlog', function() {
            // do not render value but log it in console
            return function(input) {
                console.log(input);
                return '';
            };
        });

Хоча напевне існує якийсь правильніший, вже вбудований спосіб зневадження в Angular. :)


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