Tag Archives: Павутина - Page 3

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

Простий 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

Додекалендар

Я вирішив стати майстром верстки, CSS3, SVG і всяких інших крутих штук. Для цього вирішив забабахати календар на гранях додекаедра, розгортку якого можна подивитись на моєму гітхабі: http://bunyk.github.io/dodecahedron/

Календар на гранях додекаедра вигідний тим, що це оригінально, бо 3d, а ще ним добре грати футбол чи інші ігри з м’ячем. :)

Так як я на це вбив майже половину вихідних (5 з половиною годин замість трьох запланованих), і вже не маю сил написати рендеринг власне табличок для місяців, бо треба ще вирішити чи залишати текст в тегу <pre>, чи зверстати місяці табличками, то вирішив просто написати про це тут, може хтось хто в веб-дизайні розбирається краще, щось мені порадить і прискорить роботу.

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

Найбільше сил пішло на те, щоб вирішити що React чомусь не хоче рендерити SVG, з D3 доведеться писати море коду, а Angular – саме воно, і треба його трохи підучити. Мені дуже сподобалось, надалі намагатимусь писати на ньому більше.

Розгортка і моделька

Розгортка і моделька з листка A4

А зовсім круті нерди можуть зробити аналогічний календар за допомогою Tikz в \LaTeX.


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

Вступ до D3

D3 (розшифровується як DDD, що означає Data Driven Documents) – то бібліотека для написання JavaScript візуалізацій.

Бібліотеку можна скачати з сайту, або під’єднатись до CDN:

<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>

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

d3.select('#field').text('hello world!')

Метод select приймає такі самі селектори CSS як і jQuery, тому тут нічого несподіваного для тих хто користувався jQuery (чи CSS) не повинно бути.

Можна також додавати елементи:

d3.select('body').append('p').text('hello world!')

Можна змінювати оформлення елементів:

d3.select('body')
   .append('p')
   .text('hello world!')
   .style('color', 'red')

Цих маніпуляцій з елементами вже досить щоб малювати щось з SVG:

var panel = d3.select('body');
var width = panel[0][0].clientWidth - 2;
var height = panel[0][0].clientHeight - 2;
var svg = panel.append('svg')
    .attr('width', width)
    .attr('height', height)
    .style('border', 'solid black 1px');

var circle = svg.append('circle')
    .attr('cx', width / 2)
    .attr('cy', height / 2)
    .attr('r', height / 2 - 1);

Тепер давайте додамо ще трішки кіл, і подивимось на відмінність між select та selectAll. Перший метод повертає лише перший знайдений елемент, а другий – всі.

for(var i = 0; i <= 10; i++) {
    svg.append('circle')
        .attr('r', 10)
        .attr('cy', height / 2);
};

var circles = svg.selectAll('circle');

Тепер ми можемо задати атрибут всім колам зразу. А можемо сказати що атрибут кожного кола повинен бути результатом обчислення функції. І передати замість значення – лямбду. Наприклад можна змусити наші кола скакати туди-сюди через певний інтервал:

setInterval(function() {
    circles.attr('cx', function() { return Math.random() * width });
}, 100);

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

var tabulate_function = function(f, a, b, count) {
    var f_data = [];
    var width = b - a;
    for(var i=0; i < count; i++) {
        f_data.push(f(a + i * width / count));
    };
    return f_data;
};

Передавши в функцію tabulate_function, функцію для табулювання, інтервал a, b, та кількість обчислень, ми отримуємо масив з даними:

var BARS_COUNT = 100;
var data = tabulate_function(Math.sqrt, 0, 10, BARS_COUNT);

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

var bars = svg.selectAll('rectangle').data(data); // Прив’язуємо до вибірки з прямокутників
bars.enter().append('rect'); // А що якщо прямокутників нема (не вистачає)? Тоді додаємо прямокутник.

var bar_width = width / BARS_COUNT;

bars
    .attr('y', function(d) { return height - d * 100 }) // Позиція по осі Y як і висота функції від даних
    .attr('x', function(d, i) { return i * bar_width }) // Позиція по осі X - функція від номера елементу даних.
    .attr('width', bar_width)
    .attr('height', function(d) { return d * 100 });

Має вийти щось схоже на оце: sqrt. Якщо не вийшло – подивіться в чому відмінність вашого і мого коду.

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

Щоб вони видалялись, не забудьте написати:

bars.exit().remove();

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

В такому випадку, я виношу це все в функцію update:

var update_graph = function(data) {
    var bar_width = width / data.length;
    var bars = svg.selectAll('rect').data(data);

    bars.enter().append('rect');

    bars
      .attr('x', function(d, i) { return i * bar_width })
      .attr('width', bar_width)
      .transition().duration(2000) // Наступні атрибути змінювати плавно, за 2 секунди
      .attr('y', function(d) { return height - d * 100 })
      .attr('height', function(d) { return d * 100 });
    
    bars.exit().remove();  
};

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

І на цьому напевне завершу мою розповідь, а то вона щось виходить нуднішою ніж я сподівався. А d3 не нудний, на ньому не тільки графіки можна малювати, а й наприклад прості іграшки.

Посилання

  1. JSFiddle – спонсор даної публікації. :) Всім рекомендую користуватись під час читання різноманітних підручників з веб дизайну.
  2. Scott Murray – Learning d3 (youtube)
  3. Mike Bostock – Three Little Circles
  4. Mike Bostock – Thinking With Joins

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

Шлях до JavaScript майстерності

В 2013 почав писати оцейсписок, поки мій Python-проект ще не стартував, і мені дали тимчасово розгрібати купу JS:

  1. jQuery – ok, користуватись вмію, куди без нього.
  2. JavaScript: The Definitive Guide – за один раз не прочитаєш
  3. JavaScript: The Good Parts – за один раз прочитав, але варто перечитати.
  4. JavaScript Web Applications – можливо не найцікавіша книжка яку я читав, але з неї я дізнався про Spine.js, Backbone.js, underscore.js, та паттерн pub/sub.
  5. Прочитати про те як написання коду схоже на поезію
  6. Прочитати про об’єкт Class в Leaflet. Подумати що це варто було б окремою бібліотекою випускати, бо ж не в кожному проекті де треба ООП, треба карти…
  7. Почати модульно організовувати проекти, require.js
  8. Починати скрипти з декларації "use strict";.
  9. Пам’ятати про те що існує underscore.js, писати менше циклів.

А кінця шляху нема.

Нагадування 2014-го: І Smooth Coffescript варто дочитати.


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

Заблоковано завантаження змішаного активного вмісту

З версії 23 (квітня 2013) Firefox забороняє завантаження змішаного активного вмісту, тобто ви не можете завантажувати на отримані через https сторінки скрипти (активний вміст) по http. Тому, якщо ви отримуєте таке повідомлення:

Заблоковано завантаження змішаного активного вмісту
"http://modernizr.com/downloads/modernizr-latest.js"

Треба якось захостити бібліотеку на сервері з https.

Або зайти на сторінку about:config і поставити налаштування security.mixed_content.block_active_content в false.


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

Proxylocal на CentOS

Я б Ruby поставив тільки за те, що лише вона може ганяти proxylocal. :) Proxylocal - це геніальна програма, яка дозволяє захостити будь-який локальний порт на доступному в інтернеті веб-сервері з унікальним доменом, незважаючи на те чи ви з’єднані через NAT чи через якусь іншу дупу. Досить написати:

$ proxylocal 8080
Local server on port 8080 is now publicly available via:

http://luag.t.proxylocal.com/

або

$ proxylocal 8080 --host bunyk
Local server on port 8080 is now publicly available via:

http://bunyk.t.proxylocal.com/

Інсталюється він дуже просто, якщо у вас стоїть Rubygems:

gem install proxylocal

Але якщо ні, то доведеться поставити його і ще кілька пакетів щоб все могло скомпілюватись:

yum -y install ruby gcc ruby-devel rubygems gcc-c++

make: g++: Command not found означає що бракує gcc-c++, а

mkmf.rb can't find header files for ruby at /usr/lib/ruby/ruby.h – що бракує ruby-devel.

P.S. Якщо цікавитесь Ruby – раджу оцей блог.


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

Автоматизація браузера з Selenium

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

sudo pip install selenium

З читання раджу:

А зараз я спробую написати скрипт, який робить дві речі: залишає під цією публікацією коментар і перевіряє що на сторінці з’явився вміст цього коментаря… Такий собі тест-кейс:

from time import sleep
from getpass import getpass, getuser

from selenium import webdriver

def main():
    user = raw_input('Wordpress.com username: ')
    password = getpass('Password: ')
    
    print 'Starting browser ...'
    browser = webdriver.Firefox()
    # browser.maximize_window()

    login(browser, user, password)

    print 'Opening page'
    browser.get('http://bunyk.wordpress.com/2013/10/04/selenium-browser-automation/')

    comment = 'Hello from selenium on {platform} with {name} {version}'.format(
        platform=browser.capabilities['platform'],
        name=browser.capabilities['browserName'],
        version=browser.capabilities['version'],
    )

    add_comment(browser, comment)

    assert comment in browser.page_source

    print 'Closing browser'
    browser.close()

def add_comment(browser, text):
    print 'Filling comment field ...'
    textarea = browser.find_element_by_xpath('//*[@id="comment"]')
    textarea.clear()
    textarea.click()
    textarea.send_keys(text)

    print 'Waiting till submit button slides down ...'
    sleep(1)

    print 'Submiting comment ...'
    browser.find_element_by_xpath('//*[@id="comment-submit"]').click()


def login(browser, user, password):
    print 'Logging in ...'
    browser.get('http://%s.wordpress.com/wp-login.php' % user)

    login = browser.find_element_by_xpath('//*[@id="user_login"]')
    login.clear()
    login.send_keys(user)
    pwd = browser.find_element_by_xpath('//*[@id="user_pass"]')
    pwd.clear()
    pwd.send_keys(password)
    browser.find_element_by_xpath('//*[@id="wp-submit"]').click()
    print 'Login form submitted'


if __name__ == '__main__':
    main()

Також мушу зауважити ще наступне. Firebug дуже допомагає при розробці, даючи нам xpath селектори потрібних елементів сторінки. Але, якщо ми наприклад працюємо з Ext.js, вона дає елементам дивні id на зразок DialogButton-1306-btnEl, де число змінюється випадковим чином. Тому треба писати не простий xpath, а хитро*зроблений:

    # ok = browser.xpath('//*[@id="DialogButton-1306-btnEl"]')
    ok = browser.xpath('//button[contains(., "OK")]')

P.S. Автоматизація це круто. Особливо автоматизація клацань мишкою… Хоча звісно буває не легко.


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

Цілковите пограбування блогу

І маленький експеримент щодо написання більш грамотного коду… В мене була ідея перетворювати *.py файли в публікації, а публікації – в виконувані *.py-файли, але поки що ця ідея все ще в статусі ідеї, тому тут просто буде код який можна оформити як пост:

'''
Невеликий скрипт який дозволяє завантажити собі на комп’ютер
який-небудь WordPress блог повністю. До останньої публікації.

Раніше я робив це за допомогою пошуку в ширину, MongoDB, регулярних виразів 
і багатьох інших зайвих речей. http://pastebin.com/e4SnvzAX

Зараз, дізнавшись що на wordpress.com є така 
річ як sitemap, я можу це робити набагато простіше.

'''

def main():
    '''
    Звідси програма починає роботу.
    Звісно, після того як модуль завантажить всі функції нижче.
    '''
    
    # Завантажмо наприклад мій блог:
    load_blog('http://bunyk.wordpress.com/')


def load_blog(url):
    ''' Завантажує блог в піддиректорію поточної директорії '''

    if not url.endswith('/'):
        # На всяк випадок гарантуємо що url закінчується слешем
        url = url + '/'

    # Нехай ім’я блогу - це найдовша частина url (адреса серверу зазвичай)
    blogname = max(url.split('/'), key=len)
    print(blogname)

    # Переконуємось що директорія куди ми завантажуватимемо блог існує.
    checkdir(blogname)

    sitemap = wget(url + 'sitemap.xml')

    # І зберігаємо кожну сторінку про яку пише sitemap:
    for page in iter_pages(sitemap):
        if is_article(page):
            save_page(page, blogname)

def is_article(url):
    ''' Якщо кількість слешів в url менше семи - це не стаття '''
    return sum(1 for c in url if c == '/') >= 7

def save_page(url, blogname):
    ''' Завантажити сторінку блога в файл '''
    name = url_to_name(url)
    print(name)
    with open(blogname + '/' + name, 'w', encoding='utf-8') as f:
        f.write(wget(url).decode('utf-8'))

def url_to_name(url):
    assert isinstance(url, str)
    # прибираємо адресу блогу і слеші з назви сторінки.
    name = ' '.join(url.split('/')[-4:]).strip()

    # розшифрувати кодування з %, якщо в URL кирилиця
    return unquote(name) 

try:
    from urllib.parse import unquote
except ImportError:
    from urllib import unquote


def wget(url):
    ''' Приймає url, повертає вміст того URL в кодуванні сторінки '''
    h, c = http.request(url)
    if h.status == 200:
        return c

''' Завантажувати сторінки будемо за допомогою httplib2 '''
import httplib2
http = httplib2.Http('.cache')


''' Для парсингу sitemap нам потрібна бібліотека для роботи з XML: '''
try:
    from lxml import etree
except ImportError:
    import xml.etree.ElementTree as etree

def iter_pages(sitemap):
    ''' Ця функція перетворює sitemap, даний як байти, на ітератор по url-лах '''

    assert isinstance(sitemap, bytes)

    tree = etree.fromstring(sitemap)

    ''' XPath селектор далі каже нам шукати всі елементи loc, в просторі імен
    sitemap всередині документа. '''
    return (
        element.text for element in
        tree.findall('.//{http://www.sitemaps.org/schemas/sitemap/0.9}loc')
    )


import os
def checkdir(directory):
    ''' Переконатись що директорія існує, а якщо ні - створити '''
    if not os.path.exists(directory):
        os.makedirs(directory)


if __name__ == '__main__':
    main()

Що далі?

Далі було б варто викинути html та залишити лише текст, і провести над ним якісь статистичні дослідження. Але я подумав що щоразу вручну знаходити xpath для головного вмісту сайту якось незручно і варто б це діло автоматизувати. А виявилось що виділення вмісту веб-сторінки – це тонка наука, і є задачею штучного інтелекту. І розв’язується не логічним підходом, а статистичним. Зокрема я накопав наступні роботи:

  1. 2007, alexjc The Easy Way to Extract Useful Text from Arbitrary HTML
  2. 2010, Roman Gelembjuk How to extract useful content from HTML page. Приємна несподіванка, автор статті з Франківська.
  3. 2010, Christian Kohlschütter, Peter Fankhauser and Wolfgang Nejdl: Boilerplate Detection using Shallow Text Features.

Але все це почитавши я подумав що для моїх потреб поки що XPath зійде…


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