Массовая запись с камер на выборах — 2

от автора

Хабр — не для политики. В данной статье рассматриваются исключительно технические аспекты реализации конкретного программного решения. Для всеобщего блага просьба отказаться от каких-либо политических дебатов, выступлений, агитации и тому подобных действий в комментариях. Кроме того, просьба не применять полученные знания в деструктивных целях, не начинать бекапить весь видеоархив без особой надобности и так далее. Спасибо.

8 сентября, единый день голосования. В этом году широкой общественности предлагается наблюдать через интернет за выборами столичного мэра. Ряд граждан считают для себя интересным записать картинку с камер: у кого-то политически мотивированный интерес, а большинству просто банально любопытно посмотреть на себя и знакомых глазами Интернета. Данная статья призвана продемонстировать принципы работы текущей системы и предложить работающие концепты.


Со времен прошлых выборов система немножечко изменилась (иначе и статьи бы не было), поэтому сначала вспомним, как всё работало раньше и как стало работать сейчас. Итак, у каждой камеры есть уникальный uid и пул серверов, с которых сыпется видео. Сформировав с использованием этих данных особый запрос, можно получить ссылку на кусочек видео, записанного выбранной камерой.

Для начала найдем данные обо всех существующих камерах. Мне показался наиболее простым следующий способ: начнем поиск по номеру участка, с 1 до 3800. Для этого отправим GET vybory.mos.ru/json/id_search/aaa/bbb.json, где bbb это uid, а aaa это len(bbb). Например, vybory.mos.ru/json/id_search/1/3.json

Получим json с информацией об этом участке, что-то вроде вот этого:

[{"id":7933,"name":"Участок избирательной комиссии №3","num":"3","location_id":1162,"address":"Новый Арбат, 36/9","raw_address":"г.Москва, Новый Арбат ул., дом 36/9","is_standalone":false,"size":null,"location":{"id":1162,"address":"Россия, Москва, улица Новый Арбат, 36/9","raw_address":"г.Москва, Новый Арбат ул., дом 36/9","district_id":1,"area_id":null,"sub_area_id":null,"locality_id":1,"street_id":1590,"lat":55.753266,"lon":37.577301,"max_zoom":17}}] 

Особый интерес здесь представляет id. Отправим GET вида vybory.mos.ru/account/channels?station_id=id, в данном случае vybory.mos.ru/account/channels?station_id=7933

В ответе получим строчку с кракозяблами, на которые ругается мой редактор, но содержащие внутри хеши камер и адреса серверов. Выдерем оттуда хеши регуляркой вида
\$([0-9a-h]{8}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{12}) и ip адреса регуляркой вида .*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})

В результате получим требуемую информацию о камерах текущего участка:
2e9dd8dc-edd4-11e2-9a6b-f0def1c0f84c 188.254.112.2 188.254.112.3 188.254.112.4
2ea32990-edd4-11e2-9a6b-f0def1c0f84c 188.254.112.2 188.254.112.3 188.254.112.4

Далее начинаются ньюансы. Существует три типа камер: старые, новые и отсутствующие. Чем они отличаются я расскажу чуть позже, сначала разберемся, как их различать, а различать их очень просто — нужно отправить GET вида http://SERVER/master.m3u8?cid=UID
Новая камера вернет нечто вроде

#EXTM3U
#EXT-X-VERSION:2
#EXT-X-STREAM-INF:PROGRAM-ID=777,BANDWIDTH=3145728
/variant.m3u8?cid=e1164950-0c19-11e3-803b-00163ebf8df9&var=orig

Старая камера вернет что-то такого вида:

#EXTM3U
#EXT-X-MEDIA-SEQUENCE:136
#EXT-X-TARGETDURATION:15
#EXT-X-ALLOW-CACHE:NO
#EXT-X-PROGRAM-DATE-TIME:2013-09-04T12:05:40Z
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296340.93-1378296355.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296355.93-1378296370.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296370.93-1378296385.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296385.93-1378296400.93

Отсутствующая камера не вернет ничего, кроме 404 CID Was Not Found 🙂

Теперь, когда мы умеем получать информацию о камерах конкретного участка, напишем многопоточную парсилку, которая соберет нам всю необходимую информацию. Я предпочитаю складывать данные в бесплатный монголаб, но вполне можно обойтись и обычным shelve. Зная, что участков в москве 3500+, пробежимся циклом от 1 до 3800. Ниже набросанный на коленке, но тем не менее, работающий код. В нем, разумеется, нужно вписать своё печенько и пароли от сервера монги.

# -*- coding: utf-8 -*- import json, re import httplib import threading from time import sleep import Queue from pymongo import MongoClient  client = MongoClient('mongodb://admin:кусь@кусь.mongolab.com:43368/elections')  db = client['elections'] data = db['data']  data.drop()  def get_data(uid):     print uid     headers = {'Origin': 'vybory.mos.ru',     'X-Requested-With': 'XMLHttpRequest',     'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);',     'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',     'Accept': '*/*',     'Referer': 'http://vybory.mos.ru/',     'Accept-Encoding': 'deflate,sdch',     'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4',     'Accept-Charset': 'windows-1251,utf-8;q=0.7,*;q=0.3',     'Cookie': 'rack.session=кусь'     }      try:         conn = httplib.HTTPConnection('vybory.mos.ru')         conn.request('GET', '/json/id_search/%d/%d.json'%(len(str(uid)), uid), None,headers)         resp = conn.getresponse()         try:             content = json.loads(resp.read())[0]             conn.request('GET', '/account/channels?station_id=%s'%content['id'], None,headers)             resp = conn.getresponse()             cont = resp.read()              cnt=0             for i in cont.split('\x00')[1:]:                 cnt+=1                 uid=re.findall(r'\$([0-9a-h]{8}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{12})', i)[0]                 ip=re.findall(r'.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', i)                  conn2 = httplib.HTTPConnection('%s'%ip[0])                 conn2.request('GET', '/master.m3u8?cid=%s'%(uid), None,headers)                 info = conn2.getresponse().read()                 conn2.close()                 if '/segment.ts' in info:                     camtype='old'                 elif '/variant.m3u8' in info:                     camtype='new'                 else:                     camtype='nil'                  #print content                 data.insert({                             'name':content['name'],                             'num':content['num'],                             'addr':content['address'],                             'uid':uid,                             'ip':ip,                             'cnt':str(cnt),                             'type':camtype                             })          except Exception,e:             pass      except Exception,e:         print e     conn.close()   queue = Queue.Queue() def repeat():     while True:         try:             item = queue.get_nowait()         except Queue.Empty:             break         get_data(item)         sleep(0.01)         queue.task_done()  for i in xrange(1, 3800):     queue.put(i)  for i in xrange(10):     t = threading.Thread(target=repeat)     t.start() queue.join()  print data.find().count(),'all cams' print data.find({'type':'nil'}).count(),'offline cams' print data.find({'type':'old'}).count(),'old cams' print data.find({'type':'new'}).count(),'new cams' 

Теперь у нас есть полностью собранная база камер. На момент написания статьи старых камер было 544, с ними, увы, получится работать только по-старому.
Но теперь у нас есть и 5778 новых камер, и у них есть одна особенность. Чанки со старых камер спустя очень короткое время протухают — нужно постоянно скачивать свежий плейлист, выдирать оттуда линки на чанки и качать их, пока не протухли. Новые камеры лишены этого недостатка. Можно качать чанки произвольных размеров за произвольный период времени, отправив GET вида http://SERVER/segment.ts?cid=UID&var=orig&ts=BEGINEND Между BEGIN и END может быть не 15 секунд, а гораздо больше. Я остановился на чанках длительностью 5 минут. На самом деле, можно указать хоть час, но в некоторых случаях, насколько я могу судить, если трансляция прерывалась в течение пределов чанка, не скачается весь чанк. Грубо говоря, если вы пытаетесь скачать 8 часов из архива чанками по часу и при этом в течение нескольких минут одного чанка трансляции фактически не было, не скачается весь часовой чанк. Поэтому разумно выбрать чанк поменьше. Гуру алгоритмизации (которых, как мы помним, 10%) могут написать свой бинарный поиск, дабы не пропало ни секунды видео =)
Кстати, дабы закрыть вопрос — отсутствующей называется камера, которая зарегистрирована в портале, но по факту не работает.

Автоматизируем процесс скачивания. Здесь можно было сгородить свой многопоточный велосипед на питоне, но я решил воспользоваться сторонним софтом. Мы будем генерить метафайл со ссылками на чанки для aria2c, метафайлы для tsmuxer и последовательно их запускать.

Например, вот как-то так:

# -*- coding: utf-8 -*- from time import sleep, time from pymongo import MongoClient import os import subprocess import shutil   #Корневая папка, куда будем складировать чанки directory='e:/dumps' #Размер чанка delta=300 #Номер избирательного участка num='666'   client = MongoClient('mongodb://кусь:кусь@кусь.mongolab.com:43368/elections') db = client['elections'] data = db['data']   #Качать видео за последние 8 часов start=int(time())-3600*8  #Создаем папку для дампов с избирательного участка try:     os.mkdir('%s/%s'%(directory,num)) except:     pass  #Лезем в базу и достаем оттуда информацию о камерах с участка for i in data.find({'num':num}):     if i['type']=='nil':         print 'Offline camera',i['uid']     elif i['type']=='old':         print 'Old camera',i['uid']     else:         print 'New camera',i['uid']         f=open('links-%s-%s.txt'%(num, i['cnt']),'w')         #Создаем поддиректории для каждой камеры         try:             os.mkdir('%s/%s/%s'%(directory,num,i['cnt']))         except:             pass          cur=start         files=''          #Генерируем ссылки на чанки выбранной длины         while True:             if cur+delta>time():                 for ip in i['ip']:                     url = 'http://{0}/segment.ts?cid={1}&var=orig&ts={2}.00-{3}'.format(ip,                                                                                    i['uid'],                                                                                    cur, time())                     f.write('%s\t'%url)                 f.write('\n dir={0}/{1}/{2}\n out={3}.ts\n'.format(directory,num,i['cnt'],url[-27:]))                 files += '"{0}/{1}/{2}/{3}.ts"+'.format(directory,num,i['cnt'],url[-27:])                 break             else:                 for ip in i['ip']:                     url = 'http://{0}/segment.ts?cid={1}&var=orig&ts={2}.00-{3}.00'.format(ip,                                                                                    i['uid'],                                                                                    cur, cur+delta)                     f.write('%s\t'%url)                 f.write('\n dir={0}/{1}/{2}\n out={3}.ts\n'.format(directory,num,i['cnt'],url[-27:]))                  files += '"{0}/{1}/{2}/{3}.ts"+'.format(directory,num,i['cnt'],url[-27:])              cur+=delta          #Генерируем метафайл для склеивания чанков в один большой файл.         m=open('%s-%s.meta'%(num,i['cnt']),'w')         m.write('MUXOPT --no-pcr-on-video-pid --new-audio-pes --vbr  --vbv-len=500\n')         m.write('V_MPEG4/ISO/AVC, %s, fps=23.976, insertSEI, contSPS, track=3300\n'%files[:-1])         m.write('A_AAC, %s, timeshift=-20ms, track=3301\n'%files[:-1])         m.close()          f.close()         subprocess.Popen('aria2c.exe -i links-%s-%s.txt -d %s -x 16'%(num, i['cnt'], directory), shell=True).communicate()         subprocess.Popen('tsMuxeR.exe %s-%s.meta %s/%s-%s.ts\n'%(num, i['cnt'], directory, num,i['cnt']), shell=True).communicate()         shutil.rmtree('%s/%s'%(directory,num))         os.remove('%s-%s.meta'%(num, i['cnt']))         os.remove('links-%s-%s.txt'%(num, i['cnt'])) 

Опять же, код писался исключительно в целях проверки концепта и не является образцом соблюдения PEP8, но вполне работает. Скорость скачивания по понятным причинам зависит от многих факторов.

ссылка на оригинал статьи http://habrahabr.ru/post/192590/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *