Friday, November 19th, 2010
Как правило, если вы берете VDS или Dedicated сервер, в комплект идет несколько дополнительных IP адресов, обычно 2 или 4, зависит от дата центра. Вы межете без проблем развешивать сайты на разные айпи, управляя записями DNS домена. А как запустить скрипт на не основном ip адресе сервера? Один из вариантов предложил Lorien – использовать прокси сервер – Squid. О том, как установить Squid я писал ранее. Короче говоря часто на одном ипе висит куча скриптов которые дрочат какие-то сервисы время от времени, и чтобы не натыкаться на бан или превышение лимитов API лучше всего распределить нагрузку между доступными ip адресами. Это потребует небольшой модификации скриптов, нужно будет направлять запросы через прокси. “Прозрачно” это можно сделать используя библиотеку socksipy-branch.
Чтобы перенаправлять запросы на другой ип адрес используется следующая хитрая схема : мы направляем запрос на ip:port откуда squid роутит его на айпи используя директиву tcp_outgoing_address.

Allows you to map requests to different outgoing IP addresses based on the username or source address of the user making the request.
Позволяет вам назначать запрос на другие исходящие IP адреса, основываясь на имени или адресе источника откуда юзер сделал запрос.
Осталось только настроить конфиг Squid’а нужным образом, естественно если ипов больше чем 2а лучше всего потратить 5 минут и написать скрипт который будет генерировать нужную последовательность директив :
# Говорим squid слушать нужные порты
# http_port 20000
# http_port 20001
# Создаём ACL правила
# acl p20000 myport 20000
# acl p20001 myport 20001
# С помощью созданных ACL правил выбираем нужный исходящий IP
# tcp_outgoing_address 78.109.20.226 p20000
# tcp_outgoing_address 78.109.20.227 p20001
Взял отсюда. Немного переделал приблизив скрипт к “боевым” условиям. Список айпи берет из файла ips.txt. Начальный порт задаем в start_port.
Внимание, лучше лишний раз проверить, чтобы на ипах ничего не висело. Делается это с помощью команды : netstat -tulnap
start_port = 2000
ips_list = [ item.strip() for item in open("ips.txt", "r").readlines()]
chunks = ([], [], [])
for num, ip in enumerate(ips_list):
port = start_port + num
chunks[0].append('http_port %d' % port)
chunks[1].append('acl p%d myport %d' % (port, port))
chunks[2].append('tcp_outgoing_address %s p%d' % (ip, port))
for group in chunks:
for line in group:
print line
print
print "-------------------------------\n"
Если кто знает более простой способ welcome в комменты. Я задавал это вопрос на freenode #linux (IRC канал), но практичного решения с ходу никто не предложил. Пока что единственный рабочий вариант предложил Lorien.
Posted in Code, Proxy, Python, Server | No Comments »
Thursday, November 18th, 2010
Регулярные выражения (они же регулярки, они же регэкспы) совершили настоящую революцию в обработке текстов, позволяя вести поиск и вставку текста используя так называемые паттерны – описывающие требуемый формат операции. Само по себе описание регекспов достаточно объемно, для повседневных задач можно даже изучить тему вполне поверхностно. Использовать регулярки, крайне удобно и быстро, несмотря на то что по скорости они проигрывают строковым и DOM функциям – тем не менее часто проще написать регулярку чем разрабатывать функцию. Само собой каждый уважающий себя язык программирования имеет реализацию регэкспов и Python не исключение. В качества классического примера :
def parse_proxy(page):
"""Parse proxies from webpage"""
reg = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}")
proxy_list = reg.findall(page)
return proxy_list
вытащить все прокси из кода вебстраницы. Простейшие регулярки несколько раз используются в прокси чекере.
Полезные сайты :
http://www.pcre.ru/ – типичные примеры, описание и конструктор который сильно облегчает процесс конструирования регулярных выражений.
http://www.regexlib.com/ – библиотека готовых регулярных выражений, сортировака по разделам. Очень полезный ресурс.
http://www.gskinner.com/RegExr/ – еще один конструктор регулярных выражений. Составь и проверь.
http://regexlib.ru/ – еще одна библиотека регулярок. на русском.

Использование регулярных выражений на Python.
Питон естественно предоставляет инструменты для работы с регулярными выражениям
http://docs.python.org/library/re.html
http://docs.python.org/dev/howto/regex.html – есть неплохой HOWTO
Библиотека регулярных выражений называется – re. Немаловажный момент – для строк с регулярным выражением нужно использовать префикс “r“, чтобы символы \ не преобразовывались в строковые специальные символы вроде \t, \n и т.д.
Использовать так :
match = re.search(ur"text", u"sometext and text")
print "Re.seach : ", match.group()
exp = re.compile(ur"text")
match = re.finditer(exp, u"sometext and text")
print "Re.finditer : "
for item in match:
print item.group(), item.span()
re.compile – компилирует регулярное выражение в regular expression object который может быть использован методами match() и search() и т.д. К выражению могут быть добавлены специальные флаги-модификаторы (вроде re.IGNORECASE).
Важно помнить что функции вычисляющие регулярное выражение возвращают объект у которого есть свойства и метода.
group() возвращеает строку удовлетворяющую условию регэкспа.
start() Возвращает начальную позицию совпадения
end() Возвращает конечную позицию совпадения
span() Возвращает кортеж (tuple) – (start, end) в котором заданы позиции совпадения.
Следующий пример возвращает все числа из произвольной строки :
p = re.compile('\d+')
result = p.findall('1 2 3 yolochka gori, 23 yellow popugais')
print result
Целесообразность применения регулярных выражений.
Однако не стоит чересчур увлекаться регулярными выражениями (об этом говорят даже в официальном туториале python’а), зачастую бывает проще воспользоваться строковыми функциями – они быстрее (т.к. реализованы в виде небольших, хорошо оптимизированных C циклов). В моей практике бывали случаи когда самый простой путь был – через DOM вырезать нужный кусок и уже там работать регулярками (парсинг google images, например). Представим что вам нужно заменить слово “милиция” на “полиция”, для этого вполне подойдет функция re.sub, однако в РАЗЫ проще использовать replace() :
text = "Наша милиция нас бережет."
result = text.replace("милиция", "полиция")
print result
Разница между match() и search()
Функция match() только проверяет – есть ли совпадения с начала строки, в то время как search() будет сканировать на совпадения с паттерном всю строку целиком.
print(re.match('super', 'superstition').span())
print(re.match('super', 'insuperable'))
print
print(re.search('super', 'superstition').span())
print(re.search('super', 'insuperable').span())
(0, 5)
None
(0, 5)
(2, 7)
Удачных вам регулярных выражений. Стоит уделить изучению материалов по регекспам хотя бы несколько часов, поверьте ваши вложения времени с лихвой окупятся в будущем.
Posted in Code, Notes, Python | No Comments »
Wednesday, November 17th, 2010
Эти функции позволяют вам быстро делать некоторые вещи, для которых в старом добром паскале приходилось писать лишний код. Все ниженаписанное относится к так называемому функциональному программированию, луркайте подробности.
Функции map, zip и lambda в примерах.
Простая задача есть список a = [1, 2] и список b = [3, 4] одинаковой длины и нужно слить их парами. Проще простого – используя функцию zip :
a = [1,2]
b = [3,4]
print zip(a,b)
[(1, 3), (2, 4)]
или тройками :
a = [1,2]
b = [3,4]
c = [5,6]
print zip(a,b,c)
[(1, 3, 5), (2, 4, 6)]
или в более общем виде
list = [a, b, c]
print zip(*list)
[(1, 3, 5), (2, 4, 6)]
Звездочка * перед list как-бы говорит что передается список аргументов, т.е. Действовать эквивалентно тому как если бы передали a, b, c т.е. Можно даже так print zip(*[a, b, c]) результат не изменится.
Далее функция – map. Случаются ситуации, когда внезапно нужно применить какую-либо функцию к каждому элементу списка.
Нуб напишет так :
def f(x):
return x*x
nums = [1, 2, 3]
results = []
for num in nums:
results.append(f(num))
print results
Немного поизучавший мануалы так :
def f(x):
return x*x
print [ f(num) for num in nums ]
Программист сделает проще :
def f(x):
return x*x
print map(f, nums)
А вот тру-мэдскиллз кодер сделает следующим образом :
print map(lambda x: x*x, nums)
Последняя запись являет собой пример наиболее грамотного подхода. Дело в том, что когда человек пишет код как стихи в порыве вдохновения, крайне роляет скорость написания (отсюда растут корни трепетной любви многих питон кодеров к vim и emacs), а сильная сторона питона как раз в размере генерируемого кода – он очень компактный. Написать одну строчку естественно быстрее чем 7, да и изучать короткий код проще, однако написание подобного кода требует определенного навыка. Другая сторона медали – иногда про-devs очень любят писать в одну строчку целые последовательности достаточно сложных действий, да так что очень трудно понять что там происходит и что получается в конечном итоге.
Из примера понятно, что map применяет какую-либо функцию к списку и возвращает результат опять же в виде списка. Вы можете передать несколько списков, тогда функция (идущая первым параметром) должна принимать несколько аргументов (по количеству списков переданных в map) .
def f(x, y):
return x*y
a = [1,3,4]
b = [3,4,5]
print map(f, a, b)
[3, 12, 20]
Жир, правда?
Однако если списки разной длины, т.е. Один короче другого, то он будет дополнен значениями None до нужной длины. Если убрать из списка b последнее значение – пример не будет работать, т.к. В функции f произойдет попытка умножения числа на None, а в питоне строгая типизация, что кстати выгодно отличает его от php, который в подобной ситуации работал бы дальше.
Поэтому если функция f достаточно объемна, неплохо бы проверять передаваемые значения. Например ;
def f(x, y):
if (y == None):
y = 1
return x*y
Если же заместо функции стоит None – то map действует примерно так же как и zip, но если передаваемые списки разной длины в результат будет писаться None – что кстати очень уместно в некоторых моментах.
a = [1,3,4]
b = [3,4]
print map(None, a, b)
[(1, 3), (3, 4), (4, None)]
Теперь про лямбда функции в python. Они используются когда вам необходимо определить функцию, ведь часто (как в предыдущих примерах) функция настолько мала, что определять её отдельно смыла нет (ведь это лишние строчки кода и ухудшение читабельности).
Поэтому функцию можно определить “на месте” f = lambda x: x*x как бы говорит нам – принимает x, возвращает x*x
Так используя стандартные инструменты питона можно записать довольно сложные действия в одну строчку. К примеру функцию :
def f(x, y):
if (y == None):
y = 1
return x*y
можно представить как :
lambda x, y: x * (y if y is not None else 1)
А теперь хорошо бы передавать списки отсортированные по длине – len(a) > (b) – проще простого – воспользуемся функцией sorted :
sorted([a, b], key=lambda x: len(x), reverse=True)
фунция sorted принимает список значений ([a,b] = [[1,2],[2,4,5]]) и сортирует по ключу key – который у нас задан функцией len(x) – возвращающей длину списка, сортируем в порядке убывания (reverse=True)
В конечном итоге вся операция записывается таким образом :
map(lambda x, y: x * (y if y is not None else 1), *sorted([a, b], key=lambda x: len(x), reverse=True))
списки a и b могут быть разной длины и передаваться в каком угодно порядке.
Лямбда-выражения удобны для определения не очень сложных функций, которые передаются затем другим функциям.
Posted in Code, Python | 3 Comments »
Sunday, November 14th, 2010
Как написать многопоточный прокси чекер на Python? Как нефиг делать =) Я уже писал немного о том как проверять прокси на alive, с тех пор прошло много времени и чекер немного усовершенствовался. Изначально я передал материал на форум Privatetalks, но теперь пришло время пополнить им блог. Исходные ходы поставляются как есть, в отрыве от контекста, собственно для тех кто шарит не составит труда доработать и реализовать многопоточность (на ActiveState и StackOverflow есть уже готовые решения по этой части).
Все просто. Есть какая-то страница, которую нужно запрашивать через прокси, и проверят что там отдали. Помимо этой простейшей проверки на метод GET, есть еще проверки на POST и Cookie, некоторые прокси не держат, и поэтому часто бывают бесполезны для каких-то задач. Проверка на куки мне пока была не нужна, а вот проверку на POST я сделал.
TProxy – класс который обеспечивает работу с данными прокси. Такая тема в программировании называется – инкапсуляция.
На сервере нужно разместить несколько файлов с которыми будет взаимодействовать скрипт. Можно размещать и скрипт и эти файлы на одном сервере, я так и делал =) правда тогда будьте осторожны с многопоточностью у меня скрпит в 300 потоков бодро ложил апачи (не всегда, но бывало), на неслабом железе.
Предположим это index.php :
<html>
<head>
</head>
<body><h1>SIGNATURE</h1>
<h2>Real IP : xx.xx.xx.xx</h2>
<h2>IP : xx.xx.xx.xx</h2>
</body>
</html>
SIGNATURE – уникальный идентификатор страницы. должен совпадать с CHECK_STR. Еще нужно определить 2е “константы” CHECK_URL – урл где лежит вышеприведенная страница, и CHECK_MAX_TIMEOUT – максимальный таймаут (если отклик через прокси больше, то она помечается как bad) поставьте 2.0 для начала.
Как формируются Real IP и IP? Getting real IP address in PHP – вообщем с помощью этого кода нужно сформировать Real IP и IP.
А вот тут умные дядьки пишут как чекать геолокейшн – Check GEO Location. В принципе, я так понимаю таких сервисов много и можно долбить следующий если первый не вернул адекватный результат.
def check_proxy(proxy, need_country=False):
"""Check if proxy alive + anonymity and record proxy to file if we need this"""
ip = TProxy(proxy)
gt = urllib2.build_opener(urllib2.ProxyHandler({"http":ip.get_proxy()}))
start_time = time.time()
try:
result = gt.open(CHECK_URL, timeout=CHECK_MAX_TIMEOUT)
result = result.read()
except (urllib2.URLError, socket.timeout, httplib.BadStatusLine, httplib.InvalidURL):
ip.set_alive_status(False)
return ip
except:
ip.set_alive_status(False)
return ip
ip.set_alive_status(True)
ip.set_timout(time.time() - start_time) # proxy response time
# get ip and real ip values. check for anonymity
search = re.compile(CHECK_STR)
pattern = re.search(search, result)
if pattern != None:
ip.set_alive_status(True)
search = re.compile("IP : \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
ips = re.findall(search, result)
# extract ip and real ip values from page
if ips != []:
try:
real_ip = ips[0]
given_ip = ips[1]
if real_ip == given_ip:
ip.set_anonymous_status(True)
except:
ip.set_alive_status(False)
return ip
# check country
if need_country:
try:
response = gt.open('http://api.hostip.info/get_html.php?ip=%s' % ip.get_host(), timeout=5).read()
country = re.search('Country: (.*)', response)
if country <> "":
ip.set_country(country.group(1))
else:
ip.set_country('undefined')
except:
ip.set_country('undefined')
return ip
Теперь как проверить на POST.
def generate_random_str(length=10):
str = string.lowercase+string.digits
return ''.join(random.sample(str, length))
def check_proxy_for_post(proxy):
token = generate_random_str(20)
try:
gt = urllib2.build_opener(urllib2.ProxyHandler({"http":proxy}))
post_data = urllib.urlencode({'zpost' : token})
response = gt.open(CHECK_URL + "post.php", post_data, timeout=POST_MAX_TIMEOUT).read()
# check token
print response
search = re.compile(token)
pattern = re.search(search, response)
if pattern != None:
return True
else:
return False
except HTTPError, e:
print "Http error"
return False
except URLError, e:
print "Url error"
return False
except:
print "Unknown error"
return False
Легко встраивается в функцию check_proxy приведенную выше. Нужно только немножко пошевелить мозгами.
в файл index.php добавляется форма :
<form action="post.php" "method="post">
<input type="text" name="zpost" value="post check">
<input type="submit" value="Check POST method">
</form>
В ту же папочку аккуратно ложится файлик
<html>
<body>
<h1><?php echo $_POST["zpost"]; ?></h1>
</body>
</html>
Вообщем код легко собирается в готовый продукт, добавляются фичи по вашему вкусу и вперед ура.
Кстати говоря готовый код недавно обнаружил на гитхабе – чекер прокси. Оттуда тоже можно взять несколько идей.
А еще нужно не жадничать и быть аккуратным т.к. ДЦ может абузить по подозрению в Netscan.
Понравилась статья? Зарегайся на форуме privatetalks – общайся с профессионалами.
Posted in Code, Proxy | 1 Comment »
Sunday, November 14th, 2010
Задача классическая. Удалить повторы из какого-либо списка. Самый быстрый вариант сделать так :
def uniq(seq):
"""The fastest way to unique list"""
seen = set()
seen_add = seen.add
return [ x for x in seq if x not in seen and not seen_add(x)]
Posted in Code, Python | 3 Comments »
Saturday, November 13th, 2010
Часто встречающаяся задача – получить случайную последовательность, случайной длинны из какого-либо набора данных. Например – 5-10 кейвордов из списка кеев в несколько тысяч, или выбор последовательности действия для бота имитирующего поведение человека. Ну в простейшем варианте – все просто :
import random
data = ['add_friends', 'post_story', 'vote', 'comment']
chain = random.sample(data, 2)
А если хочется задать вероятность наступления события?
import random
def get_random_chain(chain, min_max=[1,1]):
""" Get random chain from list """
items_chain = []
random.seed()
for item, num in chain.iteritems():
items_chain.extend([item] * num)
random.shuffle(items_chain)
chain_len = random.randint(min_max[0], min_max[1])
return random.sample(items_chain, chain_len)
Вот так использовать (исходная последовательность задается в виде словаря key : value – где value вероятность наступления события) :
ACTIONS = {
"post" : 20,
"vote" : 80
}
what_to_do = get_random_chain(ACTIONS, [5,10])
Получить случайную цепочку действий длинной в данном случае от 5 до 10 элементов.
Обратите внимание длинна цепочки может быть сколь угодно длинной и превышать исходную последовательность.
Posted in Code, Python | No Comments »
Saturday, November 13th, 2010
Вам нужен http://www.crummy.com/software/BeautifulSoup/ – отличная штука для работы с html/xml документами, работает даже если xml кривой. Очень часто встречающаяся задача – удалить html тэги из строки.
from BeautifulSoup import BeautifulSoup
''.join(BeautifulSoup(page).findAll(text=True))
Так же можно заюзать уже готовый код от Lorien’а (помните про open source?) – clean.py, не забудьте импортировать в проект так же и htmldata.py
Posted in Code, Python | No Comments »
Friday, November 12th, 2010
Прочитал вот это. На первый взгляд кажется – ну и что такого? Бот совершает покупки в онлайн шопе и пишет отчеты в твиттер. Тем не менее это инновация, способов применения ей можно найти массу, если приложить голову ) Такие разработки всегда привлекают внимание.
Итак собственно бот написан на Python и для поиска товаров использует API – http://developer.trademe.co.nz/api-documentation/. Бот запускается каждый день в 8 часов утра и имеет в день по 1$ “карманных денег”, котрые однако он может и не потратить (а может и потратить шанс 1/3), а сэкономить чтобы купить более дорогую покупку. Методы поиска :
Бот сканирует категории товаров, там он ищет слова – free shipping, и сортирует по самым новым поступлениям, но не более 100 итемов на категорию. Далее он сортирует по ценам стараясь потратить не больше 50% сбережений. Попутно проверяя предмет на уникальность. Полностью нехитрый алгоритм выглядит так.
The method it uses to select items:
- It has a bunch of top-level categories it looks in.
- For each of these categories, it searches for the term “Free shipping”, specifying both pay-now and buy-now, sorting by newest listings, with a maximum of 100 items returned per category.
- For each of these items, it filters on buy-now price. It tries to spend at least 50% of its savings.
- For each of the surviving items, it looks up the individual auction details to find its shipping information so it can filter on free shipping. Despite searching for the term ‘free shipping’ to start, only a small number of items have this.
- At this point I have a list of items that match the price requirements, and can be bought with a credit card buy-now.
- I then sort this list by ‘rarity’ – doing a search for the item title, and finding the item that returns the least results. As the objective here is to buy strange and esoteric things, rarity is preferred.
- Finally I buy the rarest item and subtract its cost from the bots savings.
Posted in Cool Story Bro, Fun | 2 Comments »
Wednesday, November 10th, 2010
Часто настроек в приложении может быть дофига и невольно задумываешься о том, чтобы вынести их в отдельный файл – файл настроек. Бывает программисты по инерции начинают писать код который будет считывать и обрабатывать этот файл, и бывает даже строковыми методами. Однако в питоне ничего не мешает нам создать файл вроде settings.py и исполнять его на уровне кода.
Вот к примеру файл settings.py :
import os.path as path
template_dir = path.join(path.dirname(__file__), "templates")
user_record = {
'id' : 1,
'mail' : "xyu@gmail.com",
'pass' : "1234"
}
А вот так его можно вызывать из кода :
import settings
def main():
print settings.template_dir
if __name__ == "__main__":
main()
или так (по модному) :
def main():
mod = __import__("settings")
print getattr(mod, 'template_dir')
if __name__ == "__main__":
main()
Posted in Python | No Comments »
Tuesday, September 7th, 2010
Как определить кол-во проиндексированных страниц в гугле? реализация на Python. Недавно нужно было срочно, написал :
import urllib
import re
from urllib2 import urlopen
from urlparse import urlparse
from urllib import FancyURLopener
class GOpener(FancyURLopener):
version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6'
def web_getpage(url):
g_opener = GOpener()
page = g_opener.open(url)
return page.read()
def get_index(url):
request = 'http://www.google.com/search?q=site:' + url
# About 105,000,000 results
index = 0
try:
gs = web_getpage(request)
p = re.compile('About (.*) results')
index = int(p.findall(gs)[0].replace(",", ""))
except SearchError, e:
index = -1
#print "Search Failed : %s" % e
finally:
#print "Index: %d\tURL: %s" % (int(index), url)
return index
Posted in Code, Google, Python | 2 Comments »
Thursday, September 2nd, 2010
from urllib2 import urlopen
""" Returns True if we appear to have an internet connection or False.
It defaults to using google as a test server, but you can supply an alternative if you want."""
def isonline(reliableserver='http://www.google.com'):
try:
urlopen(reliableserver)
return True
except IOError:
return False
частенько нужно проверять есть ли коннект к интернету во время выполнения какого-либо скрипта. озадачился недавно, т.к. провайдер несколько напрягал пару дней.
Posted in Code, Python | No Comments »
Tuesday, August 31st, 2010
Собственно рабочая функция проверки Google Page Rank :
import urllib, sys
def get_pagerank(url):
hsh = check_hash(hash_url(url))
gurl = 'http://www.google.com/search?client=navclient-auto&features=Rank:&q=info:%s&ch=%s' % (urllib.quote(url), hsh)
try:
f = urllib.urlopen(gurl)
rank = f.read().strip()[9:]
except Exception:
rank = 'N/A'
if rank == '':
rank = '0'
return rank
def int_str(string, integer, factor):
for i in range(len(string)) :
integer *= factor
integer &= 0xFFFFFFFF
integer += ord(string[i])
return integer
def hash_url(string):
c1 = int_str(string, 0x1505, 0x21)
c2 = int_str(string, 0, 0x1003F)
c1 >>= 2
c1 = ((c1 >> 4) & 0x3FFFFC0) | (c1 & 0x3F)
c1 = ((c1 >> 4) & 0x3FFC00) | (c1 & 0x3FF)
c1 = ((c1 >> 4) & 0x3C000) | (c1 & 0x3FFF)
t1 = (c1 & 0x3C0) << 4
t1 |= c1 & 0x3C
t1 = (t1 << 2) | (c2 & 0xF0F)
t2 = (c1 & 0xFFFFC000) << 4
t2 |= c1 & 0x3C00
t2 = (t2 << 0xA) | (c2 & 0xF0F0000)
return (t1 | t2)
def check_hash(hash_int):
hash_str = '%u' % (hash_int)
flag = 0
check_byte = 0
i = len(hash_str) - 1
while i >= 0:
byte = int(hash_str[i])
if 1 == (flag % 2):
byte *= 2;
byte = byte / 10 + byte % 10
check_byte += byte
flag += 1
i -= 1
check_byte %= 10
if 0 != check_byte:
check_byte = 10 - check_byte
if 1 == flag % 2:
if 1 == check_byte % 2:
check_byte += 9
check_byte >>= 1
return '7' + str(check_byte) + hash_str
print get_pagerank("http://twitter.com")
Posted in Code, Python | 1 Comment »
Tuesday, August 24th, 2010
Не так давно услышал про такую IDE как Pyscripter. Скачал отсюда, утсановил. Вообщем пара слов : легче чем NetBeans определенно. Рекомендую.
Posted in Code, Notes, Python | No Comments »