EBNF Parser Kata

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

На моєму домашньому модемі відвалився DSL, то я вчора ввечері не мав чим зайнятись. (Хоча звісно є купа важливіших речей) Зате біля мене була роздруківка книжечки Ніклауса Вірта “Compiler Construction“, то я взявся перекладати його парсер EBNF що ілюструє метод рекурсивного спуску з Oberon на Python. Вийшло ніби незле:

# ebnf.py
import re

class Symbol(object):
    instances = {}
    def __init__(self, name, pattern):
        self.pattern = pattern
        self.regexp = re.compile(pattern)
        self.instances[name] = self

    def match(self, text):
        text = text.lstrip()
        m = self.regexp.match(text)
        if m:
            return m.group(), text[len(m.group()):]

    @classmethod
    def get_next(cls, text):
        if not text.strip():
            return 'empty', '', ''
        for name, sym in cls.instances.items():
            m = sym.match(text)
            if m:
                return name, m[0], m[1]
        raise UnknownSymbolError('Unknown symbol: %r' % text[:50] + (
            '...' if len(text) > 50 else ''
        ))

Symbol('ident', 'w+')
Symbol('literal', '"[^"]*"')
Symbol('eql', '=')
Symbol('lparen', '(')
Symbol('rparen', ')')
Symbol('lbrak', '[')
Symbol('rbrak', ']')
Symbol('lbrace', '{')
Symbol('rbrace', '}')
Symbol('bar', '|')
Symbol('period', '.')

EBNF = '''
syntax = {production}.
production = identifier "=" expression ".".
expression = term {"|" term}.
term = factor {factor}.
factor = identifier | string | "(" expression ")" | "[" expression "]" | "{" expression "}".
'''

def parse_ebnf(text):
    sym, sym_text, remaining = '', '', text
    
    def next():
        nonlocal sym, sym_text, remaining
        sym, sym_text, remaining = Symbol.get_next(remaining)

    next()
    
    def syntax():
        res = ['syntax']
        while sym == 'ident':
            res.append(production())
        return res

    def production():
        name = sym_text
        next()
        if sym == 'eql':
            next()
        else:
            raise ExpectedEqualityError
        exp = expression()
        if sym == 'period':
            next()
        else:
            raise NoPeriodError
        return ['production', name, exp]

    def expression():
        res = ['expression', term()]
        while sym == 'bar':
            next()
            res.append(term())
        return res

    def term():
        res = ['term']
        while sym in ('ident', 'literal', 'lparen', 'lbrak', 'lbrace'):
            res.append(factor())
        if len(res) == 1:
            raise NoFactorError
        return res

    def factor():
        if sym == 'ident':
            res = [sym, sym_text]
            next()
            return res
        elif sym == 'literal':
            res = [sym, sym_text]
            next()
            return res
        elif sym == 'lparen':
            next()
            exp = expression()
            if sym == 'rparen':
                next()
            else:
                raise NoRParenError
            return ['(', exp]
        elif sym == 'lbrak':
            next()
            exp = expression()
            if sym == 'rbrak':
                next()
            else:
                raise NoRBrakError
            return ['[', exp]
        elif sym == 'lbrace':
            next()
            exp = expression()
            if sym == 'rbrace':
                next()
            else:
                raise NoRBraceError
            return ['{', exp]
        else:
            raise RuntimeError(
                "term should be called only when "
                "sym in ('ident', 'literal', 'lparen', 'lbrak', 'lbrace')"
            )

    return syntax()

class UnknownSymbolError(SyntaxError):
    pass

class NoPeriodError(SyntaxError):
    pass

class NoRParenError(SyntaxError):
    pass

class NoRBrakError(SyntaxError):
    pass

class NoRBraceError(SyntaxError):
    pass

class NoFactorError(SyntaxError):
    pass

class ExpectedEqualityError(SyntaxError):
    pass

Трохи TDD, трохи дописування тестів після:

# test.py
from unittest import TestCase, main, skip

from ebnf import Symbol
from ebnf import parse_ebnf, EBNF
from ebnf import print_tree
from ebnf import (
    NoPeriodError, NoRParenError, UnknownSymbolError,
    NoRBrakError, NoRBraceError, NoFactorError
)

class TestSymbol(TestCase):
    def test_paren(self):
        self.assertEqual(
            Symbol.get_next('(asdf)'),
            ('lparen', '(', 'asdf)')
        )
    def test_ident(self):
        self.assertEqual(
            Symbol.get_next('asdf'),
            ('ident', 'asdf', '')
        )
    def test_bar(self):
        self.assertEqual(
            Symbol.get_next('|asdf'),
            ('bar', '|', 'asdf')
        )

class TestEBNF(TestCase):
    def test_self(self):
        tree = parse_ebnf(EBNF)
        self.assertEqual(
            parse_ebnf(EBNF),
            ['syntax',
             ['production',
              'syntax',
              ['expression',
               ['term', ['{', ['expression', ['term', ['ident', 'production']]]]]]],
             ['production',
              'production',
              ['expression',
               ['term',
                ['ident', 'identifier'],
                ['literal', '"="'],
                ['ident', 'expression'],
                ['literal', '"."']]]],
             ['production',
              'expression',
              ['expression',
               ['term',
                ['ident', 'term'],
                ['{', ['expression', ['term', ['literal', '"|"'], ['ident', 'term']]]]]]],
             ['production',
              'term',
              ['expression',
               ['term',
                ['ident', 'factor'],
                ['{', ['expression', ['term', ['ident', 'factor']]]]]]],
             ['production',
              'factor',
              ['expression',
               ['term', ['ident', 'identifier']],
               ['term', ['ident', 'string']],
               ['term', ['literal', '"("'], ['ident', 'expression'], ['literal', '")"']],
               ['term', ['literal', '"["'], ['ident', 'expression'], ['literal', '"]"']],
               ['term', ['literal', '"{"'], ['ident', 'expression'], ['literal', '"}"']]]]]
        )

    def test_without_period(self):
        with self.assertRaises(NoPeriodError):
            parse_ebnf('symbol = "asdf"')

    def test_without_rparen(self):
        with self.assertRaises(NoRParenError):
            parse_ebnf('symbol = ("asdf"')

    def test_without_rbrak(self):
        with self.assertRaises(NoRBrakError):
            parse_ebnf('symbol = ["asdf"')

    def test_without_rbrace(self):
        with self.assertRaises(NoRBraceError):
            parse_ebnf('symbol = {literal')

    def test_unknown_symbol(self):
        with self.assertRaises(UnknownSymbolError):
            parse_ebnf('!symbol = "asdf"')

    def test_no_factor(self):
        with self.assertRaises(NoFactorError):
            parse_ebnf('sym = ||')

    def test_for_binary(self):
        self.assertEqual(
            parse_ebnf('''
                digit = "0"|"1".
                number = digit {digit}.
            '''),
            ['syntax',
             ['production',
              'digit',
              ['expression', ['term', ['literal', '"0"']], ['term', ['literal', '"1"']]]],
             ['production',
              'number',
              ['expression',
               ['term',
                ['ident', 'digit'],
                ['{', ['expression', ['term', ['ident', 'digit']]]]]]]]
        )


if __name__ == '__main__':
    main()

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

digit = "0" | "1"
number = digit {digit}

first(number) = {"0", "1"}

І ми знаємо що парсер може прочитати не будь-яку граматику, а лише ту, в якої для кожного виразу на зразок

definition = exp1 | exp2

Виконується умова:

first_1(exp1) \cap first_1(exp2) = \varnothing

Таким чином отака граматика багатозначна:

sum = number | sum ("+" | "-") sum

Бо вираз 1 – 10 + 11 можна розпарсити як (1 – 10) + 11 або як 1 – (10 + 11). А все тому, що first(number) = first(sum).

Її можна переписати так щоб в правилі для суми не було двох варіантів що починаються з однакових символів.

sum = number {("+" | "-") number}

Так вся сума розгортатиметься зразу і не буде проблем з тим яку операцію виконувати швидше.

Тепер би ще змусити його автоматично генерувати код парсера за EBNF і вийде власний YACC. :)


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

Пишемо переглядач молекул з Pyglet

Я хотів створити серію уроків про графіку в OpenGL по слідах NeHe, але отримав іншу пропозицію, і пріоритети змінились. Ну й графіка в наш час людей не так цікавить. Але так як задачу я почав робити, просто витирати її з списку проектів буде не цікаво, краще опублікувати те що є і перенести в список закінчених проектів. Чим я зараз й займусь.

Ідея програми – намалювати атоми сферами різних кольорів і розмістити їх в різних місцях простору, таким чином отримавши молекулу. Для цього нам треба знати координати. Для цього ми використаємо Open Babel – хімічну експертну систему. Ось інструкції з інсталяції, apt-get install python-openbabel якщо кому лінь їх читати.

Глюкоза

Молекула глюкози

Користуючись нею, ми можемо перетворити формулу SMILES, на список координат атомів:

import pybel

smile = raw_input('Enter SMILE molecule:')
molecule = pybel.readstring('smi', smile)
molecule.make3D()

for atom in molecule.atoms:
    print atom.type, ' '.join(map(str, atom.coords))

SMILES можна знайти в статтях вікіпедії про різні речовини. Ось наприклад глюкоза: OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O. І її координати:

O3 3.08232699168 1.41136753836 1.97383867659
C3 2.63605783234 0.116724346362 2.37092125466
C3 2.87897272901 -0.854338624684 1.21070216538
H 2.47296741411 -0.36351143938 0.319191621385
O3 4.29331227545 -1.03408976253 1.03052197614
C3 2.18198168708 -2.2062426467 1.4123437219
H 2.52686439483 -2.70828022178 2.32628341407
O3 0.757046712076 -2.07375697965 1.48816465135
C3 2.4834103428 -3.09613160782 0.198042289604
H 2.03158506981 -2.66605342385 -0.704362586236
O3 1.84366255476 -4.36331585678 0.373985237387
C3 3.99532722341 -3.24522583591 0.00523555683618
H 4.41250004827 -3.77444425583 0.871020029013
O3 4.29626886035 -4.03252960027 -1.15195954921
C3 4.62954125722 -1.84273135567 -0.0947112115177
H 5.71887138656 -1.95252927548 -0.108137148969
O3 4.3079363412 -1.18316983652 -1.31789119536
HO 2.91473185309 2.02688340774 2.71060639162
H 1.56956736943 0.19992674022 2.5985040442
H 3.17829013287 -0.18365904297 3.27321534863
HO 0.532102901531 -1.64213847492 2.33074495499
HO 0.907383263524 -4.15051515566 0.55676359202
HO 3.89297302828 -3.59188323274 -1.91963488602
HO 3.3506978517 -1.05986229308 -1.37211748251

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

#! /usr/bin/python3
from random import random

import pyglet
from pyglet.window import key, Window
from pyglet.gl import *
from pyglet.gl.glu import *

window = Window()

Об’єкт наступного класу просто буде повертати кортеж з кольором для кожної назви атома. Деякі атоми ми задамо вручну, щодо правильної палітри – дивіться статтю CPK coloring.

class Palette(object):
    def __init__(self):
        self.colors = {
            'H': (0.0, 0.5, 0.5),
            'HO': (1.0, 0.5, 0.5),
            'C3': (0.1, 0.1, 0.1),
            'Car': (0.1, 0.1, 0.1),
            'O3': (1.0, 0.0, 0.0),
        }

    def get_color(self, name):
        if name not in self.colors:
            print(name)
            self.colors[name] = (random(), random(), random())
        return self.colors[name]

palette = Palette()

Молекула – це по суті список атомів (кожен з яких четвірка з назви і трьох координат), що буде завантажувати себе з файлу при створенні екземпляру класу, і вміє малювати себе:

class Molecule(object):
    def __init__(self, fn):
        self.atoms = []
        with open(fn) as f:
            for l in f:
                el, x, y, z = l.split()
                self.atoms.append(
                    (el, float(x), float(y), float(z))
                )

    def draw(self):
        for atom in self.atoms: # для кожного атома
            glPushMatrix() # зберегти матрицю моделі
            glTranslatef(*atom[1:]) # змістити матрицю моделі в координати атома
            # намалювати сферу радіусу 1 і кольору відповідного типу атома
            draw_sphere(1, palette.get_color(atom[0]))
            glPopMatrix() # завантажити збережену матрицю моделі

molecule = Molecule('glucose.dat') # створити молекулу глюкози

def draw_sphere(radius, color):
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
    
    # довго пояснювати що таке колір матеріалу, я й сам не до кінця знаю.
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, (GLfloat * 3)(*color))
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION,
        (GLfloat * 3)(*map(lambda x: x/2, color))
    )
    # glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (GLfloat * 3)(*color))

    sphere = gluNewQuadric()
    gluSphere(sphere, radius, 50, 50) # 50, 50 - це кількість меридіанів та паралелей. 
    # якщо потрібно багато атомів - зменшіть їх кількість для збільшення продуктивності.

Тепер займемось власне перемальовуванням екрану:

@window.event
def on_draw():
    update_frame(0)

rotation = 0 # Глобальна зміна з поточним поворотом моделі
def update_frame(dt):
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity() # завантажити матрицю ідентичності 

    global rotation
    rotation += dt * 10 # чим більше часу пройшло - тим більше повертаємо
    glRotatef(rotation, 0, 1, 0) # навколо осі y
    molecule.draw() # і малюємо нашу молекулу.

При зміні розмірів вікна (і при його створенні) ініціалізуємо всілякі налаштування OpenGL:

@window.event
def on_resize(width, height):
    glClearColor(0.0, 0.3, 0.0, 0.0) # задаємо колір фону

    glEnable(GL_DEPTH_TEST) # вмикаємо буфер глибини

    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glLightf(GL_LIGHT0, GL_POSITION, 1, 5, 4) # ставимо одне світло

    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, width / height, .1, 1000) # перспективна проекція з кутом 45
    gluLookAt( # ставимо камеру і націлюємо її в цент сцени
     1, 4, 15, # eye
     0, 0, 0, # target
     0, 1, 0  # up
    )
    glMatrixMode(GL_MODELVIEW) 
    return pyglet.event.EVENT_HANDLED

При натисканні клавіш “вліво” і “вправо” оновлюємо кадр, повернувши трішки модель. А також оновлюємо 50 разів на секунду. І запускаємо цикл подій:

@window.event
def on_key_press(symbol, modifiers):
    if symbol == key.LEFT:
        update_frame(-1)
    elif symbol == key.RIGHT:
        update_frame(1)

pyglet.clock.schedule_interval(update_frame, 0.02)

pyglet.app.run()

На цьому і все. Можна було звісно написати набагато краще, без глобальних змінних, з кращими поворотами камери і кращим освітленням і т.п. Але поки що є важливіші речі. (Хоча, якщо ви захочете онлайн курс, і зможете зробити так що мені не треба буде ходити на роботу – можемо щось придумати ;) ).

Ах, і стаття з якої взято інформацію про те як отримати координати для атомів молекули: Patrick Fuller – Molecules in Blender


Filed under: Графіка, Кодерство Tagged: освіта, OpenGL, Python

Шпаргалка по Docker

Docker в порівнянні з гіпервізором другого типу.

Docker в порівнянні з гіпервізором другого типу.

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

Віртуальні машини корисні для ізоляції середовища. Наприклад середовища розробки. Хоча цим може займатись і Vagrant. А ще Vagrant може керувати не тільки машинами на VirtualBox, а й контейнерами Docker. Коли що використувати – здається справа особистих вподобань. Хоча мені кажуть що так як контейнери більш легковісні, тут інша філософія роботи, наприклад “кожному процесу свій контейнер”. Ось цікаве обговорення питання що коли варто використовувати, в якому беруть участь автор Vagrant та автор Docker.

Але краще раз попробувати ніж сто разів прочитати:

Інсталяція

На Linux найпростіше, хоч і не безпечно:

wget -qO- https://get.docker.com/ | sh

Запуск контейнера

bunyk@ubuntu:~$ docker run docker/whalesay cowsay boo 
Post http:///var/run/docker.sock/v1.19/containers/create: dial unix 
/var/run/docker.sock: no such file or directory.
Are you trying to connect to a TLS-enabled daemon without TLS?

Якщо бачите таку помилку – значить або докер ще не запущений:

bunyk@ubuntu:~$ sudo service docker start

Або ваш користувач не знаходиться в групі докера:

sudo usermod -aG docker bunyk

Різні інші дії

# скачати образ (щоб він не качався коли ми будемо робити йому run)
docker pull

# список всіх скачаних образів
docker images

# всі запущені контейнери
docker ps

# всі (не лише запущені) контейнери
docker ps -a

# показати лише ідентифікатори контейнерів
docker ps -q

 # видалити всі контейнери
docker rm $(docker ps -aq)

# останній запущений контейнер
docker ps -l

# видалити контейнер
docker rm

# видалити образ
docker rmi

# запустити інтерактивну програму в контейнері
docker run -t -i debian /bin/bash

# запустити демона в контейнері
docker run -d debian /bin/sh -c "while true; do echo hello world; sleep 1; done"

# запустити контейнер так, що директорію хоста /host/dir буде змонтовано як /container/dir
docker run -v /host/dir:/container/dir debian

# слідкувати за логами демона в контейнері
docker logs -f ecstatic_lovelace

# збудувати образ з Dockerfile поточної директорії
docker build -t ouruser/ourrepo .

Filed under: Інструменти, Кодерство Tagged: linux

OpenGL в Python

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

Інсталяція та перше вікно

Найперше що потрібно графічним програмам – вікно. Щоб створити вікно, нам треба якусь бібліотеку, наприклад PyQt, PySide, PyGtk, WxPython чи PyGame – їх купа. Потрібно також щоб це вікно підтримувало контекст OpenGL (тобто могло дозволити відеокарті виводити свої дані в область вікна). З цим може справитись багато бібліотек, але ми виберемо Pyglet. Тому що в ній мало зайвого, і вона ставиться традиційно:

pip install pyglet

Ну, і як годиться – почнемо з найпростішої програми:

import pyglet

window = pyglet.window.Window(width=640, height=480, caption="Hello OpenGL!")
pyglet.app.run()

Отримаємо вікно заданої ширини та висоти, і з заданим заголовком:

Наше перше вікно

Наше перше вікно

Елементарно, правда?

Фарби

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

import pyglet
from pyglet.gl import * # імпортуємо всі функції OpenGL
# вони починатимуться з префіксів gl або glu, тому простір імен надто не засмічуватимуть

window = pyglet.window.Window(width=640, height=480, caption="Hello OpenGL!")

# я не буду довго пояснювати що таке декоратор. Просто знайте, що 
# @window.event позначає функції що відповідають за обробку подій

@window.event
def on_draw(): 
    # викликатиметься, коли операційна система вирішить що вікно треба перемалювати
    # наприклад, коли ми забрали вікно що було над нашим, або вперше виводимо його на екран  

    glClearColor(1.0, 1.0, 1.0, 1.0) # Задати колір яким ми будемо очищати екран. 
    # Четверте число - прозорість.
    # Я його сам не дуже розумію, але обов’язково треба чотири параметри.

    glClear(GL_COLOR_BUFFER_BIT) # очистити буфер кольору 
    # (бувають і інші буфери, але про це пізніше)

pyglet.app.run()

To be continued

В цьому уроці я хотів ще написати про те як намалювати трикутник, але часу мало (тобто є інші пріорітети). Зате ми створили вікно і навчились змінювати його колір. Ну й краще напевне писати менше але частіше. Якщо пілотний епізод цього курсу буде популярний – подумаю чи випускати перший сезон.

P.S. Май на увазі, якщо ти не хочеш навчити свою дівчину програмувати – вона може знайти когось хто схоче. :P Або взагалі сама з допомогою інтернету звикне вчитись.


Filed under: Графіка, Кодерство Tagged: OpenGL, Python

D3: фіксація вузла в графі

Knowledge is power – it’s measured in WAT?!s (Rose Ames)

Я все мрію написати детективну історію на зразок “Шерлок Холмс і Бага Баскервілів”, але ніяк не підберу багу аби сюжет був достатньо гостросюжетним. Ось наприклад поділюсь знанням цікавої особливості D3.js при малюванні графів за допомогою фізичної симуляції. Наприклад ми хочемо деякі вузли графа розміщувати вручну.

Документація пише:

Each node has the following attributes: …
fixed – a boolean indicating whether node position is locked.

Ок, тоді давайте при кліку по вузлі, показувати меню, в якому є галочка “фіксувати ноду”. В коді що показує меню пишемо:

pin_down.setChecked(data.fixed);

І чомусь, не залежно від того фіксований вузол, чи ні, галочка завжди стоїть:

pin

Я почав логувати data.fixed, виявилось що вона отримує значення то 6, то 7, то 3… Зовсім не булевські. Я думав вже що десь якийсь код думає що fixed на вузлі графа означає щось інше.

Документація бреше. Виявилось що fixed – не булева змінна.

Internally, the force layout uses three bits to control whether a node is fixed. The first bit can be set externally, as in this example. The second and third bits are set on mouseover and mousedown, respectively, so that nodes are fixed temporarily during dragging. Although the second and third bits are automatically cleared when dragging ends, the first bit stays true in this example, and thus nodes remain fixed after dragging. (Sticky Force Layout).

Ну що ж, давайте по масці виділяти перший біт:

pin_down.setChecked(data.fixed && 1);

Все одно завжди поставлена? Ааа, ну так, && – це логічна кон’юнкція, а не побітова. 6 && 1 – true. Нам треба побітову.

pin_down.setChecked(data.fixed & 1);

Resolve issue -> Fixed.

Тепер залишилось придумати як в такий детектив додати персонажів і саспенсу. :D


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

Анонс Lvivpy4

Попередні три я пропустив бо не читаю новини, але от на цьому буду, тому що там я буду нести людям світло науки і знання.

Цитую сам себе:

Ви дізнаєтесь що таке компонент, інтерфейс, інваріант, утиліта, фабрика, адаптер (це не тільки шаблони), реєстр компонентів, і купу інших розумних слів. А також як можна застосовувати штуки що ці слова позначають щоб побудувати систему яку можна розширювати новими компонентами, і реалізувати “статичну типізацію” та “слабку типізацію”.

Одним словом те що ви могли почитати в трактаті про ZCA, тільки тепер в авторській озвучці, + можна буде задавати питання, і побачити мене в 3D. Головне руками не чіпати! :)

Тому якщо в кого виникне таке бажання – заходьте в Офіс Lohika Systems, Львів, вул. Лемківська 15а, 2-й поверх, 30-го травня. Реєстрація: http://www.meetup.com/uapycon/events/222342688/

І не переживайте, там доповідаю не тільки я.

Робота і життя після відпустки починається шалено (правда якщо врахувати що під час відпустки я більшість часу лише спав, їв і дихав), я навіть 10% всього цікавого зараз не розповів, але те що розповів – одне з найголовнішого. :)


Filed under: Кодерство, Нещоденник Tagged: Python

Ethernet на хлопський розум

I’m a web crawling spider; you an Internet mosquito;
You thought the 7-layer model referred to a burrito.
You’re a dialup connection; I’m a gigabit LAN.
I last a mythical man-month; you a one-minute man.

Monzy – “kill -9

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

Рівні мережевої моделі

Почнемо з рівнів. Комп’ютерні мережі такі складні, що зрозуміти їх цілком дуже важко, тому вирішили розділити їх на рівні абстракції. Цих рівнів можна нарахувати від п’яти до семи, але ми почнемо з перших двох.

Перший рівень – фізичний. На цьому ми домовляємось скільки вольт – це нуль, а скільки – одиничка, чи може це взагалі буде лазер в оптоволокні, або радіохвиля. Використовувати частотну, чи амплітудну модуляцію? Це все фізичні аспекти. Також цей рівень за допомогою різноманітних кодувань, вирішує як довго триває одиничка або нуль, і як відрізнити послідовність 11111 від послідовності 111111?

Таким чином, якщо ми обрали середовище фізичного рівня – виту пару, радіо, чи оптоволокно, ми знаємо що якщо ми передамо 01, то хтось прийме 01 і саме в такому порядку.

Далі йде другий рівень – канальний. Так як і в спілкуванні людей, треба серед всіляких звуків середовища розрізняти слова, особливо сказані до тебе, так і тут, треба серед усіх нулів і одиничок треба виділити окремі повідомлення, і визначити хто з ким говорить. Для цього домовляються про послідовності біт на зразок “Dixi” (я все сказав), “Шо?” (здається лінія зашумлена, повторіть передачу). І “так я сказав те що ви почули” (контрольна сума).

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

Контроль доступу до середовища

Визначенням того хто коли говорить займається підрівень (який взагалі то другий, але його чомусь виділяють) Media access control (контроль доступу до середовища, MAC), і є різні способи:

  • Наприклад передавати по колу токен (мікрофон), і давати право говорити лише тому, в кого токен. Після чого він має передати токен наступному вузлу мережі, і так далі по колу. Така схема називається Token ring.
  • Переважно люди які знаходяться в одній кімнаті спочатку слухають, і якщо ніхто не говорить – починають говорити. Якщо сигнал передано успішно – добре, а якщо хтось почав говорити одночасно з нами (сигнал поширюється середовищем певний час) – доведеться почекати випадковий час і спробувати ще раз.

    Такий протокол називається Carrier sense multiple access (CSMA), 1-persistent. Перекладається як множинний доступ з відчуттям носія і зухвальством-1. Відчуття носія означає що ми слухаємо провід. Зухвальство 1 означає що ми ліземо вставити свої 5 копійок, як тільки випаде нагода. Це не оптимально щодо кількості колізій. Протокол зі зухвальством 0.5 з ймовірністю 50% замість того щоб передавати, вирішує почекати і послухати ще один такт. Тому що коли на лінії одна машина передає, і своєї черги чекають 10, то з зухвальством буде дуже важко добитись наступної передачі без колізій.

    В Ethernet (найпопулярніший протокол для побудови мережі) використовується CSMA/CD – Carrier Sense Multiple Access with Collision Detection, тобто те саме що я описав, тільки вони вміють виявляти колізію за формою сигналу, ще до того як передасться ввесь кадр з контрольною сумою.

Ethernet

Тепе поговоримо детальніше про стандарти першого і другого рівня – Ethernet. Ehternet – це назва стандарту IEEE 802.3, і частка Ether – (етер), там на честь ефіру (якого насправді не існує, лише вакуум).

Але Ethernet існує і його створили в 1976 Роберт Меткальф та Девід Боґґс в дослідницькому центрі Xerox. (Ця компанія так багато винайшла! Шкода що їх знають лише за копіювальними апаратами). Швидкість передачі була 3 Мбіт/с.

В 1978 Dell, Intel та Xerox створили стандарт DIX, який описував 10 Мбіт/с Ethernet.

В 1983-му стандарт DIX став називатись IEEE 802.3. Меткальф заснував власну компанію 3com і почав продавати Ethernet адаптери.

Ethernet має обмеження на довжину кабеля, яке можна обійти за допомогою повторювачів – пристроїв фізичного рівня, які просто посилюють сигнал. З точки зору канального рівня кабель з повторювачами не відрізняється від звичайного. Але все одно є обмеження на час передачі сигналу, після якого не можливо усунути колізії.

По кабелю Ehternet сигнал передається в манчестерському коді. Інформація формується в кадри, формат яких описано на вікіпедії.

Для роботи колізій використовували CSMA/CD зухвалості 1. Станція слухає канал і якщо чує тишу – передає. Якщо сталась колізія чекає випадкову кількість інтервалів. Інтервал = 2t, де t – час протягом якого сигнал поширюється від одного кінця кабелю до іншого. Якщо кількість колізій 10 – очікують від 0 до 1023 інтервалів. Якщо i > 16 – повертають помилку.

Комутований Ethernet

2550T-PWR-Front
З часом користувачі Ehternet виявили що приєднання купи комп’ютерів до одного кабеля має недолік, що обрив кабеля псує роботу всієї мережі. І придумали концентратор (хаб). Концентратор відрізняється від повторювача тим, що має більше ніж два порти, а так принцип той самий – сигнал який входить на один порт, виходить на всіх інших підсиленим до потрібного рівня. З точки зору канального рівня мережа з концентратором виглядає як один кабель, всі комп’ютери під’єднані до концентратора містяться в одному просторі колізій, а тому чим більше комп’ютерів в мережі, тим повільніше вона працюватиме.

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

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

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

Fast, Gigabit Ethernet

Після того як з’явився комутований Ethernet, швидкість мережі перестала падати за нелінійним законом при приєднанні нових комп’ютерів і люди почали звикати до швидкості 10 Мбіт/c.

В 1992-му IEEE доручив комітету 802.3 створити новий стандарт, помноживши швидкість на 10. В 1995-му вийшов стандарт 802.3u, відомий в народі як Fast Ethernet. Який дозволяв передавати дані зі швидкістю 100 Мбіт/с. Свічі звісно мусили підтримувати обидва стандарти і щоб визначити який з них можна застосовувати на порті використовували процедуру яка називається “autonegotiation”.

Далі Fast Ethernet звісно став недостатньо швидким, і його знову вирішили помножити на 10. В 1999 IEEE випустили стандарт 802.3ab, або Gigabit Ehternet, що передавав 1 Гбіт/с, як не важко було здогадатись. В ньому заборонили використовувати концентратори і дуже скоротили кабелі (максимальна довжина 25м), тому що при такій швидкості можна відправити цілу купу кадрів до того як проявляться перші ознаки того що відбувається колізія. (мінімальний розмір кадру – 64 байти.)

Але 25 метрів це дуже мало, тому щоб збільшити, до стандарту дописали що можна дописувати ще одне поле, яке розширяє кадр до 512 байт (carrier extension) і використовувати кабелі до 200м. Але передавати 512 байт, де корисні лише 64 дуже неефективно. тому також пробували склеювати кадри в пакети (frame bursting).

Також в Gigabit Ethernet є функція контролю потоку. Якщо кадри йдуть з Gigabit в класичний Ethernet, свіч може послати передавачу на Gigabit порті кадр PAUSE (кадр в якому в полі тип написано 0x8808), щоб передавач пригальмував.

А далі, в 2002, 2004 та 2006 роках з’явились стандарти 10-гігабітного Ethernet для оптоволокна, екранованого мідного кабелю і мідної витої пари. І працюють над ще швидшими! Але ми в ті деталі сьогодні не вдаватимемось.

Мости

Коли ми розглядаємо мережі, треба завжди розглядати два аспекти – фізичний і логічний. Фізичний – це те як ми прокладаємо кабелі і які пристрої ними з’єднуємо. Логічний – як кадри всередині кабелів рухаються і комутуються насправді. Ми можемо як розділити одну фізичну мережу на кілька логічних, так і з’єднати кілька фізичних в одну логічну. Мости займаються останнім.

До появи комутованого Ethernet, мережа – це був кабель, в який комп’ютери вгризались за допомогою пристрою що називався “вампірчик” (прочитайте про кабель 10BASE5). Але кабель має обмежену довжину, і йому дозволено обмежену кількість повторювачів. Так само організація може займати кілька приміщень, і помістити всі комп’ютери в одну мережу, хай навіть і за допомогою комутатора, буде накладно. В таких випадках кожне приміщення отримує фізичну мережу, які з’єднуються між собою за допомогою моста.

Міст працює в нерозбірливому режимі (promiscuous mode), тобто приймає всі кадри, не залежно від того кому він призначався. І далі вирішує що з ним робити, шукаючи потрібну інформацію в спеціальній таблиці.

При приєднанні моста до мереж, ця таблиця порожня. Як тільки на якийсь з портів приходить кадр, міст запам’ятовує в таблиці MAC-адресу відправника і номер порта, і розсилає кадр на всі порти окрім того на який кадр прийшов (як хаб).

Якщо хтось відповідає, і просить прислати кадр на MAC-адресу яка мосту вже відома, міст шле кадр лише на відому адресу.

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

У вас напевне виникло питання “А чим відрізняється міст і комутатор?”. А тим самим що й ноутбук та лептоп – в слові комутатор більше букв :). Просто дві різні назви з’явились історично.

Метою мостів було з’єднувати різні мережі (наприклад Token Ring і Ethernet), але виходило погано, тому переважно вони з’єднували однокабельні Ethernet мережі, або Ethernet мережі з концентраторів. А метою комутаторів було замінити концетратори, які мали обмеження на число з’єднань пов’язане з котролем доступу до середовища. Але в кінці вийшло одне й те ж.

Spanning tree protocol (STP)

Слово “міст” (bridge) використовується й досі, тому що його використовують в актуальних досі стандартах, наприклад IEEE802.1D. Це стандарт що описує те як повинні працювати мости, і зокрема, такі деталі як протокол кістякового дерева.

Ми можемо з’єднувати два мости (концентратори) більш ніж одним кабелем для збільшення надійності з’єднання. Але тоді з’являється проблема – широкомовні кадри зацикляться, бо міст отримуватиме кадр через один порт і відправлятиме на всі інші, таким чином, якщо два мости з’єднані через два порти, вони пересилатимуть цей кадр одне одному, і всім іншим приєднаним пристроям до нескінченності.

Тому, мости домовляються ігнорувати лінії які не входять в кістякове дерево. Для цього, обирається кореневий свіч (свіч з найменшим ID, яким є MAC), і знаходяться всі найкоротші (найшвидші) лінії які ведуть від кожного пристрою до кореневого свіча. Це й є кістякове дерево.

Радья Перлман що придумала цей алгоритм змогла також віршик написати:

A tree that must be sure to span
So packets can reach every LAN.
First, the root must be selected.
By ID, it is elected.
Least-cost paths from root are traced.
In the tree, these paths are placed.

Взагалі, хотів би я вміти писати вірші. Їх справді легше й веселіше запам’ятовувати. Саме за допомогою вірша я вперше зміг запрограмувати рендеринг фракталу Мандельброта.

VLAN

Окрім того що багато фізичних локальних мереж можна об’єднати в одну логічну, можна також поділити одну фізичну, на багато логічних. Причин для цього є кілька:

  • Безпека (ізоляція даних)
  • Зменшення навантаження при доставці широкомовних пакетів. Такі пакети часто зустрічаються, бо потрібні наприклад для роботи протоколу ARP.
  • Локалізація можливого %D0%BE%D0%B2%D0%BD%D0%B8%D0%B9_%D1%88%D1%82%D0%BE%D1%80%D0%BC">широкомовного шторму та інших неполадок.
  • Працівник може перевестись з одного офісу в інший (наприклад ближче до дому), і потрапити в іншу фізичну мережу. Але йому все одно треба бути в логічній мережі свого відділу.

Всі ці проблеми намагається вирішити технологія VLAN (віртуальної локальної мережі).

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

Що робити якщо порт прибуває через порт що належить кільком VLAN? Кадр може належати лише одній мережі, але якій? Для цього, IEEE погодився змінити формат кадру Ethernet і додати туди нове поле. Стандарт випущений в 1998 назвали IEEE 802.1Q.

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

VLAN який присвоюють кадру визначають за тим з якого порта він прийшов, або за протоколом вищого рівня. Наприклад можна послати всі кадри що містять IP в одну мережу, а кадри з PPP – в іншу. Було б зручно також визначати VLAN за MAC-адресою (наприклад якщо ноутбук часто змінює порти), але стандарт 802.1Q такого не передбачає.

Поле ідентифікатора для VLAN займає 12 біт. Таким чином загальна кількість можливих VLAN – 4096. Цього недостатньо для хмарних сервісів які дозволяють створювати мільйони віртуальних серверів і мереж, тому для них придумали іншу технологію віртуалізації – VXLAN (Virtual eXtensible LAN). Але в ній вже самі кадри віртуальні і передаються всередині TCP-пакетів. Віртуалізується віртуальна мережа. :)

Вищі рівні

Мости/комутатори це пристрої другого рівня. Вони дивляться на заголовки кадрів і таким чином вирішують що з ними робити. Концентратори і повторювачі – пристрої першого рівня. Вони просто приймають і передають сигнали, не особливо задумуючись над тим що ті сигнали означають.

З’єднати мережі з різними протоколами складно, бо переклад навіть чогось формального – дуже складна задача. Це ускладнюється різним максимальним розміром порції даних дозволених для передавання (MTU), різною схемою адресації і багато чим іншим. Але крім трансляції одного протоколу в інший, є ще один спосіб.

Щоб справді з’єднати мережі різного типу (наприклад 802.11 (Wi-Fi), 802.3 (Ethernet) та MPLS (швидкісний протокол що з’єднує комутатори провайдерів)), треба використати ще один рівень абстракції. Навіть якщо на другому рівні пристрої адресуються не за допомогою MAC, вони все одно адресуються спільними адресами третього рівня (наприклад IP). На цьому рівні абстракції працює маршрутизатор (router). Він відкидає всі заголовки кадрів другого рівня, і дивиться в їх вміст. А вміст – це датаграма третього рівня, і її заголовки вона описують куди її відправити.

Існують також пристрої ще вищого рівня, які називаються шлюзами. Існують шлюзи транспортного рівня, що дозволяють перетворювати дані з одного протоколу в інший, або шлюзи прикладного рівня, що, наприклад перетворюють SMS-повідомлення в E-mail.

Джерела

Всіх посилань не даю, бо їх забагато. В основному “Комп’ютерні мережі” Танненбаума і вікіпедія.

До читачів

Cподіваюсь я не переобтяжив ваші мізки інформацією і загальні принципи ви зрозуміли. Дякую що дочитали сюди і не заснули. І пробачте що так мало картинок – мені треба оновити свою стару Xubuntu 12.04, і подивитись чи в ній працюватиме графічний планшет. Тоді сподіваюсь цей блог стане більш ілюстрованим.

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


Filed under: Конспекти, Павутина Tagged: hardware

Генштаб для грамнацистів

Щойно з’явилась ідея супер-пупер сервісу для всіх хто пише і вичитує.

Сценарії використання

  1. Користувач бачить помилку на якомусь сайті. (Ідея націлена на вікіпедію, але має працювати на будь-яких інших сайтах, наприклад тут.) Нехай він зареєстрований на нашому порталі грамнацистів, і встановив букмарклет. Користувач виділяє ту помилку, натискає на букмарклет, той букмарклет показує попап з формою, де вписаний рядок з помилкою, полем куди можна ввести правильний варіант, пояснення помилки і посилання на словники та правопис.
  2. Користувач пише статтю. Він хоче написати її грамотно. Він вводить її текст, натискає інший букмарклет, і той використовуючи алгоритм Ахо-Корасік чи щось легше, підкреслює всі помилки і в попапах показує коментарі користувачів що їх виправили, та варіанти виправлень.
  3. Користувач вважає що помилка – не помилка, і попередній користувач помилився додавши її до словника. В попапі він клацає кнопочку “обговорення” і переходить на наш портал, де може обговорити.
  4. Адміністратори/редактори (вікіпедії) вирішують хто правий, а хто ні. Помилка яка насправді такою не була, позначається, і при наступних спробах її ввести, йому будуть повідомляти що це не помилка, і якщо він не згоден – посилати в обговорення.
  5. Найпопулярніші помилки, які вже точно помилки експортуються в файл що читається роботом який лазить по вікіпедії і виправляє всі статті та перевіряє всі нові правки. Якщо користувач вніс правку з помилкою – йому приходить повідомлення з поясненням суті помилки, проханням так не робити і порадою поставити букмарклет. :) На зразок такого:

  6. Треба врахувати те, що помилок існує більше ніж правильних слів (наприклад в слові “пиво” можна помилитися 32^4 = 2^{20} \approx 10^6 мільйоном різних способів, і то якщо не враховувати що також існують пропуск, дефіс, апостроф та інші символи. Тому варто намагатись вводити лише часті помилки, а решту залишати на hunspell. Словник для hunspell теж дозволити редагувати спільно, через букмарклети.

P.S. Я ще можу змиритись з тим що Firefox мені підкреслює hunspell, latex, букмарклет, попап і подібне. Але “блозі”, “кнопочку” “P.S.” і т.п. міг би вже вивчити.


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

Випадковий ідентифікатор в Python

Можна отримати так:

import random
def random_id(length=6):
   return ''.join(
        random.choice(string.lowercase)
        for i in range(length)
    )

###############
>>> random_id()
'kqxmua'

Якщо треба особливо оформлений, як от IP, чи MAC-адреса, то можна зробити перетворення:

def asmac(val):
    """Convert a byte string to a MAC address string.  """
    return ':'.join('%02X' % ord(c) for c in val)

def random_mac():
    return asmac(random_id())

###################
>>> random_mac()
'78:71:6A:72:6E:63'

Але такі ідентифікатори як “kqxmua” нормальній людині важко запам’ятати, бо вони не асоціюються з жодними поняттями. Ну окрім частинки “ua”, але й то вона туди випадково потрапила. Проте, в Linux можна легко отримати випадкове слово, бо в ньому є словник:

def random_word():
    return random.choice(
        open('/usr/share/dict/words').readlines() # жертиме пам’ять! 
    ).strip()

#################
'.join(random_word() for i in range(5))
'hermitical, Canter, Paryavi, mergences, Mind'

Хоча я знайомий лише з “hermitical” та “mind”, але асоціації вже легше побудувати, правда?


Filed under: Всяке, Кодерство Tagged: linux, Python

Зберегти всю стіну групи VK в таблицю

Якось під час чергового наближення економічної кризи захотілось проаналізувати ціну нерухомості в Львові. А ще, в той же час я наткнувся на документацію API Вконтакті. А так як своє житло я шукав в тому числі і в тій соціальній мережі, то вирішив проаналізувати наприклад спільноту vk.com/nomakler.

Ну, проаналізувати це легко сказати – важче зробити. Як витягти з повідомлення ціну, і як відрізнити попит від пропозиції? Га?

Але є половинний результат – ФСБ мусило поділитись частиною своєї бази даних розміром в 40450 оголошень. Тут можна її завантажити як tsv, xls чи інший зручний для вас формат. Може комусь, хто захоче збільшити конкуренцію серед львівськи маклерів/ріелторів знадобиться.

А якщо кому потрібна інформація з інших груп – ось скрипт. Викликаєте функцію save2tsv з назвою групи і назвою файлу в який писати – і чекаєте поки завантажиться.

import requests
import json
from pprint import pprint
from itertools import islice
from datetime import datetime

from butils.csv_wrapper import UnicodeWriter

class APIError(Exception):
    pass

def vk(method, **kwargs):
    '''
        https://vk.com/dev/methods
    '''
    r = requests.get(
        'https://api.vk.com/method/%s' % method,
        params=kwargs
    )
    js = json.loads(r.text)
    if js.get('error'):
        raise APIError(js['error']['error_msg'])

    return js['response']


def get_users(ids, known_users={}):
    request_ids = [i for i in ids if i not in known_users]
    if request_ids:
        user_ids=','.join(str(i) for i in request_ids if i > 0)
        if user_ids:
            users = vk('users.get', user_ids=user_ids)
        else:
            users = []
        group_ids=','.join(str(-i) for i in request_ids if i < 0)
        if group_ids:
            groups = vk('groups.getById', group_ids=group_ids)
        else:
            groups = []
        for user in users:
            known_users[int(user['uid'])] = dict(
                first_name = user['first_name'],
                last_name = user['last_name'],
            )
        for group in groups:
            known_users[-int(group['gid'])] = dict(
                first_name = group['name'],
                last_name = group['gid']
            )
    return known_users


def get_wall(domain):
    count = 50
    offset = 0
    def get_with_offset(offset):
        nonlocal count
        print('get_with_offset(%s)' % offset)
        total = vk('wall.get',
            domain=domain,
            count=1,
        )[0]
        off = total - offset - count
        if off < 0:
            count = count + off
            off = 0
        return vk('wall.get',
            domain=domain,
            count=count,
            offset=off,
        )[1:][::-1] # remove first and reverse

    while True:
        posts = get_with_offset(offset)
        offset += count
        users = get_users(p['from_id'] for p in posts)
        for p in posts:
            yield p, users[p['from_id']]
        if count < 50:
            return


def save2tsv(domain, dst):
    with UnicodeWriter(dst, encoding='utf-8', delimiter='\t') as writer:
        writer.writerow((
            'ID',
            'Datetime',
            'First name',
            'Last name',
            'Text',
            'Type',
            'Comments',
            'Reposts',
            'Likes',
        ))
        for p, user in get_wall(domain):
            writer.writerow(list(map(str, (
                p['id'], # ID
                datetime.fromtimestamp(p['date']), # Datetime
                user['first_name'], # First name
                user['last_name'], # Last name
                p['text'], # Text
                p['post_type'], # Type
                p['comments']['count'], # Comments
                p['reposts']['count'], # Reposts
                p['likes']['count'], # Likes
            ))))

# save2tsv('nomakler', 'nomakler.tsv')

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