Як відсканувати книжку без сканера?

Є класний додаток для андроїд від Microsoft – Office Lens. Він робить деякі зусилля щодо того аби криво (під якимось кутом) сфотографовані документи виглядали як відскановані. Це звісно важче для книжки, особливо якщо багато сторінок обдерті і не прямокутні, але часто виглядає краще ніж просто фото, і потім обрізати менше.

Знімки програми потім можна буде знайти на пристрої за шляхом /Pictures/Office Lens.

А зліпити їх до купи і перетворити в DJVU – за допомогою скрипта, який я запозичив звідси і трохи модифікував:

import os, glob, subprocess

#Change these to suit your situation=========================
IMGDIR="./" #directory of images to be converted
OUTDJVU = IMGDIR + 'OUT.djvu'

#Don't change these ==========================================
TMPDJVU = IMGDIR + 'TMP.djvu'


#convert jpg to djvu and collate to a single file   
if os.path.exists(OUTDJVU):
    os.remove(OUTDJVU)

for infile in sorted(glob.glob(os.path.join(IMGDIR, '*.jpg'))):
    print('Processing ' + infile)

    #convert jpg to a temp djvu file
    # cmd = 'c44 -decibel 48 ' + '"'+infile+'"' + ' "'+TMPDJVU+'"'
    subprocess.call(['c44', '-decibel', '48', infile, TMPDJVU])
    
    if os.path.exists(OUTDJVU):
        #Add the djvu file to the collated file
        cmd = ['djvm', '-i', OUTDJVU, TMPDJVU]
    else:
        # Create the collated file
        cmd = ['djvm', '-c', OUTDJVU, TMPDJVU]
    subprocess.call(cmd)

#Delete the temporary file
os.remove(TMPDJVU)

print('\nAll files converted and collated successfully')

В результаті можна отримати щось отаке: https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B8%D1%82%D0%B0%D0%BD%D0%BA%D0%B0_%D0%B4%D0%BB%D1%8F_II._%D0%BA%D0%BB._%D1%88%D0%BA._%D1%81.djvu

Програмістські експерименти

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

– Чи копіюється map-а, при присвоєнні?
– Хвилину.

package main

import "fmt"

func main() {
fmt.Println("- Чи копіюється map-а, при присвоєнні?")
a := make(map[string]string)
a["answer"] = "Так"
b := a
b["answer"] = "Ні"
fmt.Println("-", a["answer"])
}

– Ні

Якщо звісно не запитання “нам обрати технологію А чи Б?” І звісно ресурсу на те щоб реалізувати рішення в обох і порівняти нема. Тоді й з’являються релігійні суперечки про те в кого мова потужніша.

Як склеїти всі pdf-файли в один?

Сьогодні дізнався команду яка допоможе коли наприклад дружина прислала тобі 5 сканів в файлах pdf, мережеве з’єднання до принтера не працює, принтер вміє друкувати файли з флешки, але вибираючи кожен файл поокремо в меню принтера можна стерти пальці, і хочеться спростити собі життя. Знайшов на AskUbuntu:

gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=all.pdf *

Плюси (для мене) цієї команди порівняно з іншими варіантами:

  • Виявилася вже встановлена на Ubuntu
  • gs (GhostScript) – це інтерпретатор екзотичної тюрінг-повної мови програмування PostScript, яка є чимось середнім між TeX та Forth. Завжди корисно як не вивчити нову мову то хоча б її штрикнути одним пальцем.
  • Автором цієї утиліти є чувак про якого я колись читав книжку і не одну.

Символи вікіпедії, детальніший аналіз

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

Загалом статті складаються з 3 613 435 448 символів, з яких різних 23 717. Вони розподілені так (намальовано лише початок розподілу, бо решта – неозрброєним оком не видно):

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

1) пробіл (32) 442790853
2) а 160531292
3) о 157877564
4) н 138215454
5) і 117277229
6) р 105263631
7) и 98886082
8) е 88928818
9) т 87767363
10) | 80797095
11) в 78200503
12) с 75999527
13) ] 73604421
14) [ 73599732
15) л 70900301
16) к 69356359
17) новий рядок (10) 57091749
18) e 52089242
19) у 50198598
20) д 48912768
21) = 46361768
22) м 46138865
23) t 41512840
24) п 40914157
25) a 39627848
26) r 36179188
27) n 36087343
28) я 34788627
29) ь 33853333
30) . 32022232
31) s 31881844
32) i 30972299
33) з 30652689
34) o 30492305
35) 1 30006539
36) l 29367163
37) г 28746761
38) 0 27570049
39) й 26579527
40) - 24869609
41) ' 24751079
42) б 23634039
43) 2 21423701
44) , 21261521
45) / 20051917
46) c 19845819
47) p 19831300
48) ц 19183628
49) } 18895464
50) { 18889983
51) ч 18359390
52) d 17813691
53) b 16517162
54) 9 16240786
55) g 15262611
56) х 14936744
57) u 14902347
58) f 14303148
59) ї 13406838
60) h 13050086
61) m 12543340
62) < 12111655
63) : 12102999
64) > 12094377
65) ж 11923903
66) 3 10947193
67) 5 10647152
68) " 10484943
69) 4 10091884
70) w 9560852
71) К 9364738
72) П 9190333
73) 8 9177988
74) ю 9157643
75) ; 9037575
76) 6 8918805
77) С 8870091
78) ) 8867662
79) ( 8851064
80) ш 8821755
81) 7 8188158
82) ф 7340920
83) А 7166375
84) y 7039008
85) & 7006502
86) В 6869441
87) _ 6022842
88) М 5970386
89) є 5956858
90) k 5778567
91) Г 5745696
92) * 5712005
93) — 5519856
94) Л 4852833
95) S 4825612
96) Р 4712055
97) v 4646612
98) Н 4635296
99) Д 4580191
100) A 4422679
101) D 4409130
102) Т 4356396
103) щ 4143981
104) Б 4112548
105) У 4017973
106) R 3910061
107) Ф 3848824
108) I 3805943
109) « 3732952
110) » 3730320
111) C 3706725
112) О 3626277
113) E 3501507
114) P 3206479
115) І 3168901
116) F 3042662
117) З 2886284
118) L 2875664
119) T 2762480
120) % 2668723
121) B 2665154
122) M 2605927
123) N 2471967
124) G 2455406
125) # 2432361
126) x 2377037
127) ! 2307947
128) z 1683501
129) H 1652473
130) V 1627598
131) j 1580981
132) K 1542325
133) Ч 1534632
134) Х 1497880
135) U 1430170
136) Ш 1413847
137) O 1364521
138) Е 1326679
139) W 1071605
140) Ц 970737
141) – 873533
142) J 866094
143) ? 838918
144) Q 767843
145) Я 759389
146) Є 680158
147) X 636113
148) Y 617782
149) Ж 607830
150) ы 548375
151)   511929
152) Ю 503103
153) q 490449
154) + 467382
155) ́ 381075
156) \ 343889
157) 328026
158) Z 313673
159) ґ 293475
160) Й 286295
161) № 279189
162) é 263700
163) И 240639
164) Ґ 138039
165) ° 134968
166) ’ 129122
167) Щ 112586
168) ½ 92609
169) э 89526
170) … 88401
171) ó 87628
172) â 84646
173) ~ 75512
174) ü 67713
175) ^ 66068
176) ł 65770
177) Э 65262
178) ă 63282
179) á 62358
180) ъ 60515
181) ţ 60459
182) Ї 59899
183) • 58273
184) ² 57176
185) − 51293
186) × 51201
187) ё 50264
188) ä 49539
189) “ 47666
190) ö 47657
191) $ 46091
192) † 42959
193) ş 41864
194) è 41182
195) α 39774
196) í 38230
197) „ 34637
198) ο 34016
199) → 28048
200) ا 27473
201) · 26648
202) ‎ 26007
203) ν 24624
204) ń 24312
205) ι 24220
206) ę 24171
207) Ь 24064
208) ± 23585
209) ′ 23492
210) ” 23461
211) τ 23408
212) ș 23387
213) ς 23318
214) ρ 22519
215) š 21622
216) ś 21499
217) à 21334
218) ą 20463
219) ў 19269
220) ​ 18949
221) ε 18309
222) č 18232
223) @ 18014
224) É 17552
225) ç 17282
226) λ 17177
227) ل 16987
228) ³ 15756
229) σ 14667
230) κ 14317
231) ż 13948
232) η 13811
233) μ 12411
234) ر 12083
235) ي 11783
236) ā 11684
237) ñ 11460
238) م 11324
239) ć 11226
240) υ 11130
241) ن 11115
242) ß 10974
243) π 10708
244) ί 10461
245) ë 9850
246) ı 9824
247) ú 9761
248) ž 9598
249) ά 9572
250) ț 9284
251) γ 9152
252) ‘ 9122
253) ա 8975
254) ѣ 8947
255) و 8929
256) қ 8841

Ось вони ж, по групах:

Український алфавіт

2) а 160531292
3) о 157877564
4) н 138215454
5) і 117277229
6) р 105263631
7) и 98886082
8) е 88928818
9) т 87767363
11) в 78200503
12) с 75999527
15) л 70900301
16) к 69356359
19) у 50198598
20) д 48912768
22) м 46138865
24) п 40914157
28) я 34788627
29) ь 33853333
33) з 30652689
37) г 28746761
39) й 26579527
42) б 23634039
48) ц 19183628
51) ч 18359390
56) х 14936744
59) ї 13406838
65) ж 11923903
71) К 9364738
72) П 9190333
74) ю 9157643
77) С 8870091
80) ш 8821755
82) ф 7340920
83) А 7166375
86) В 6869441
88) М 5970386
89) є 5956858
91) Г 5745696
94) Л 4852833
96) Р 4712055
98) Н 4635296
99) Д 4580191
102) Т 4356396
103) щ 4143981
104) Б 4112548
105) У 4017973
107) Ф 3848824
112) О 3626277
115) І 3168901
117) З 2886284
133) Ч 1534632
134) Х 1497880
136) Ш 1413847
138) Е 1326679
140) Ц 970737
145) Я 759389
146) Є 680158
149) Ж 607830
152) Ю 503103
159) ґ 293475
160) Й 286295
163) И 240639
164) Ґ 138039
167) Щ 112586
182) Ї 59899
207) Ь 24064

Англійський алфавіт

18) e 52089242
23) t 41512840
25) a 39627848
26) r 36179188
27) n 36087343
31) s 31881844
32) i 30972299
34) o 30492305
36) l 29367163
46) c 19845819
47) p 19831300
52) d 17813691
53) b 16517162
55) g 15262611
57) u 14902347
58) f 14303148
60) h 13050086
61) m 12543340
70) w 9560852
84) y 7039008
90) k 5778567
95) S 4825612
97) v 4646612
100) A 4422679
101) D 4409130
106) R 3910061
108) I 3805943
111) C 3706725
113) E 3501507
114) P 3206479
116) F 3042662
118) L 2875664
119) T 2762480
121) B 2665154
122) M 2605927
123) N 2471967
124) G 2455406
126) x 2377037
128) z 1683501
129) H 1652473
130) V 1627598
131) j 1580981
132) K 1542325
135) U 1430170
137) O 1364521
139) W 1071605
142) J 866094
144) Q 767843
147) X 636113
148) Y 617782
153) q 490449
158) Z 313673

Цифри

35) 1 30006539
38) 0 27570049
43) 2 21423701
54) 9 16240786
66) 3 10947193
67) 5 10647152
69) 4 10091884
73) 8 9177988
76) 6 8918805
81) 7 8188158

Пунктуація

1) пробіл (32) 442790853
17) новий рядок (10) 57091749
10) | 80797095
13) ] 73604421
14) [ 73599732
21) = 46361768
30) . 32022232
40) – 24869609
41) ‘ 24751079
44) , 21261521
45) / 20051917
49) } 18895464
50) { 18889983
62) < 12111655
63) : 12102999
64) > 12094377
68) ” 10484943
75) ; 9037575
78) ) 8867662
79) ( 8851064
85) & 7006502
87) _ 6022842
92) * 5712005
93) — 5519856
109) « 3732952
110) » 3730320
120) % 2668723
125) # 2432361
127) ! 2307947
141) – 873533
143) ? 838918
151) нерозривний пробіл (160) 511929
154) + 467382
155) ́ 381075
156) \ 343889
157) TAB (9) 328026
161) № 279189
165) ° 134968
166) ’ 129122
168) ½ 92609
170) … 88401
173) ~ 75512
175) ^ 66068
183) • 58273
184) ² 57176
185) − 51293
186) × 51201
189) “ 47666
191) $ 46091
192) † 42959
197) „ 34637
199) → 28048
200) ا 27473
201) · 26648
202) символ зліва-направо 26007
208) ± 23585
209) ′ 23492
210) ” 23461
220) пробіл нульової довжини (8203) 18949
223) @ 18014
228) ³ 15756
252) ‘ 9122

Латиниця з наворотами

162) é 263700
171) ó 87628
172) â 84646
174) ü 67713
176) ł 65770
178) ă 63282
179) á 62358
181) ţ 60459
188) ä 49539
190) ö 47657
193) ş 41864
194) è 41182
196) í 38230
204) ń 24312
206) ę 24171
212) ș 23387
215) š 21622
216) ś 21499
217) à 21334
218) ą 20463
222) č 18232
224) É 17552
225) ç 17282
231) ż 13948
236) ā 11684
237) ñ 11460
239) ć 11226
242) ß 10974
245) ë 9850
246) ı 9824
247) ú 9761
248) ž 9598
250) ț 9284

Застарілі кириличні букви

150) ы 548375
169) э 89526
177) Э 65262
180) ъ 60515
187) ё 50264
219) ў 19269
254) ѣ 8947
256) қ 8841

Грецькі букви

195) α 39774
198) ο 34016
203) ν 24624
205) ι 24220
211) τ 23408
213) ς 23318
214) ρ 22519
221) ε 18309
226) λ 17177
229) σ 14667
230) κ 14317
232) η 13811
233) μ 12411
240) υ 11130
243) π 10708
244) ί 10461
249) ά 9572
251) γ 9152

Арабські

227) ل 16987
234) ر 12083
235) ي 11783
238) م 11324
241) ن 11115
255) و 8929

Вірменська

253) ա 8975

Парні символи

Минулого разу мені стало цікаво чому кількість дужок не співпадає. Може варто було б написати правильний алгоритм зі стеком, але я поки напевне спробую знайти статті де просто кількість дужок не співпадає аби подивитись в чому річ.

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

То отримаємо нескінченний потік помилок у всіх статтях:

Швеція Закриваюча дужка ] без відкриваючої до неї біля "eritage Foundation]]]:\n* ВВП&nbsp;— $ 24"
Сумська область До кінця сторінки не закриті наступні дужки [[
Тернопільська область Закриваюча дужка ] без відкриваючої до неї біля "ьні вечори. Дебют]]»]] (Тернопіль), міжн"
Теліга Олена Іванівна Закриваюча дужка ] без відкриваючої до неї біля "нього шкільного віку] / [[Анна Багряна]]"
Західний Буг До кінця сторінки не закриті наступні дужки [
Андрухович Юрій Ігорович Закриваюча дужка ] без відкриваючої до неї біля ". Літ, 29 січня 2013]</ref> були наведен"
Данило Галицький Закриваюча дужка } без відкриваючої до неї біля "а Романівна]] ({Пом}} після [[1241]]), о"
29 Закриваюча дужка } без відкриваючої до неї біля "377a.htm}}{{ref-en}}}</ref>\n\n== Народили"

Переважкно люди в джерелах ставлять зайву закриваючу квадратну, або забувають відкрити квадратну, тому що якось дуже недбало ставляться до оформлення джерел. Але тих помилок так багато що я не знаю як їх всіх виправити. 😦 Може варто написати якийсь лінтер для вікіпедії, який би про ці помилки авторам повідомляв?

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

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

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

Як помиготіти клавіатурою ноутбука Dell в Linux

Для тих кому мало гірлянди на свята 😉 . Якщо в файл /sys/devices/platform/dell-laptop/leds/dell::kbd_backlight/brightness записати ціле число від 0 до 2, то це встановлює відповідну яскравість підсвітки клавіатури. Наприклад:

import time

with open('/sys/devices/platform/dell-laptop/leds/dell::kbd_backlight/brightness', 'w') as brightness:
    i = 0
    while True:
        i += 1
        time.sleep(0.2)
        brightness.write(str(i % 3))
        brightness.flush()

Filed under: Кодерство Tagged: linux

Простеньке 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