E民主主義、またはモスクワの改修のための投票(および投票率)に関するデータを収集および処理する方法

改修プログラムへの入場または退場の投票が完了すると、何らかの理由で、特定の各住宅の外観に関するデータがモスクワ市長のサイトから消え、全体として賛成票と反対票のみが残りました。 もちろん、いくつかの数字はニュースに書かれていますが、あなたは本当に自分でそれらを見て、統計をいじり、グラフを作成したいのですか?



はい、次のようなステートメントの後:

これらのサービスの人気に関しては、My Documentsの公共サービスセンターは投票者全体の半分をやや超えており、Active Citizenポータルにわずかに負けています。
どういうわけかわずかな疑問があります。 だから-情報の収集を始めましょう! そして、それを分析します。 これを行うには、ある種の言語(たとえばpython)、ある種のデータベース(たとえばsqlite)、およびある種のWebスクレーパーが必要です。これらはpython用のものが多いためです。 最後に、結果のデータベースへのリンクを提供しますが、それを使って何でもできるとすぐに言わなければなりません。



市役所の敷地に行き、車輪を回します。 4543の家からデータを収集する必要があります。 地区リストランダムな家をクリックして、データの一般的な形式を確認します。



どうやら、すべての家には特定のIDがあり、URLで確認できます。



https://www.mos.ru/otvet-stroitelstvo/itogi-golosovaniya-zhitelej-po-proektu-programmy-renovacii/?u=121







したがって、すべてを自宅で処理するには、すべての識別子のリストを取得して整理する必要があり、各ページから必要な情報、つまり投票数、反対票数、合計投票数を取得します(「未決定」もあります) 「投票、あるアパートで彼らの半分が賛成し、半分が反対する場合)、および一般的な家の会議があったかどうか(なぜですか?



IDのリストはどこで入手できますか? 上で見たように、地区リストには指定された家へのリンクはありません。これは単なる住所のテキストリストであり、それだけです。 残念、それは便利でしょう。 ここで並べ替えを行う必要があるようです。 ただし、まず、特定の各識別子からデータを収集する関数を作成します。



まず、家の取得リクエストを作成し、結果の回答を見て、そこから何をスクラップするべきかを理解します。



 import requests r = requests.get('https://www.mos.ru/otvet-stroitelstvo/itogi-golosovaniya-zhitelej-po-proektu-programmy-renovacii/?u=121') print(r.text)
      
      





応答に投票データがないことを知って驚いた。ある種のページテンプレートが与えられた。 結果のダウンロード方法を見てみましょう。 そして実際には-最初にブラウザに未入力のフィールドがあるページがあり、しばらくしてからデータが表示されます。 「開発者向け」の適切なツール(Mozillaなど)を備えたブラウザーを使用して、そこで何が起こるかを正確に確認してください。







うん! いくつかのapiに対するブラウザリクエストが見つかりました。IDは監視している家のIDと同じです。



つまり、パラメーターなしでwww.mos.ru/altmosprx/api/1/renovation/house_result/121へのgetリクエストを送信すると、この種類のJSONが返されます。



 { "execTime": 0.044450044631958, "errorMessage": "", "result": { "table": "<table class="table table-big"><thead><tr class="table-header"><td> </td><td> </td><td></td><td></td><td> </td></tr></thead><tbody><tr><td class="apartment-id">0G6O4</td><td class="voting-info"><p>bf659227e8e3</p><p>5f9403659209</p></td><td class="voting-choice"><p></p><p></p></td><td class="voting-date"><p>18.05</p><p>18.05</p></td><td class="apartment-position apartment-agree"><p></p></td></tr><tr><td class="apartment-id">0G6O5</td><td class="voting-info"><p>3f12be5cea77</p></td><td class="voting-choice"><p></p></td><td class="voting-date"><p>15.05</p></td><td class="apartment-position apartment-agree"><p></p></td></tr><tr> ... <td class="apartment-id">0G6V1</td><td class="voting-info"><p>5acd126a410ea1a842e67066ea68fa8f</p></td><td class="voting-choice"><p></p></td><td class="voting-date"><p>24.05</p></td><td class="apartment-position apartment-agree"><p></p></td></tr></tbody></table>", "total": { "und": 0, "za": 100, "protocol_res": 0, "protiv": 0, "gorod_mark": 0, "protocol_date": null, "house_status": 1, "gorod": 0 }, "und_table": "<table class=\"table table-big\"><thead><tr class=\"table-header\"><td> </td><td> </td><td></td><td></td><td> </td></tr></thead><tbody></tbody></table>", "address": " ,  63,  2" }, "request_id": "empty_requestid", "errorCode": 0 }
      
      





さて、ここでは何も廃棄する必要はありません。すべてのデータはAPIから直接取得されます。 必要なのは、総会に関するアパートメントIDの数(総投票数)を計算し、投票率に関する既製のデータを取得することだけです。



しかし、それでも、どの範囲のIDを見るのでしょうか? 合計で4543の家があるはずです。api0を指定します-そのような家があります。 -1を与える-エラー、まあ、ありがとう。 下限が決定されました。 私たちは10000を与えます-そのような家があります。 さて、はっきりと4543があります。最近参加した領土からいくつかの家を見て、上部の境界線をおおよそ決定してみましょう...地図に戻り、「古い」ものから「新しいモスクワ」のどこかに行きます。約:ココシキノ集落、ココシキノ休暇村、労働者通り、家2、id:440931。まあ、少なくとも50万人がいます。



50万のリンクを通常のループで整理するのは最善のアイデアではないため、 concurrent.futuresモジュールを使用ます。 もちろん、asyncioのようなものを使用することも可能ですが、そのような大規模なタスクはなく、「小さな血」で行うことができます。 すべてが非常に簡単です。 家番号が明らかに正しい場合、明らかに間違っている場合、apiが提供するものを調べ、すべてをチェックするための関数を作成します。 次に、並列クエリを使用してすべてをループでねじります。 IDはかなり処理する必要があります。 次に、結果を作成して書き留めます。 一般に、次のコードのようなものが得られます。



 import requests from concurrent.futures import ProcessPoolExecutor import concurrent.futures def check(url): #try  ,   -,    ,     #             try: r = requests.get('https://www.mos.ru/altmosprx/api/1/renovation/house_result/' + str(url) + '/', timeout=10) print(url) r.encoding = 'utf-8' if '400: Bad Request' not in r.text: return str(url) except: #  ,        ,      woops = str(url) + ' failed' return woops results = [] with ProcessPoolExecutor(max_workers=6) as executor: future_results = {executor.submit(check, url): url for url in range(0, 1000000)} #   ,      : url in somelist for future in concurrent.futures.as_completed(future_results): results.append(future.result()) results[:] = [item for item in results if item or item == 0] #check  None,   , ;     ,     with open('/home/deb/mosres.txt', 'w') as f: for item in results: f.write('{}\n'.format(item))
      
      





私たちはビジネスに取り掛かります-これは6人の労働者でさえ長い間です。 将来的には、100万を処理した後、結果よりも70軒少ない家を手に入れたので、このバグパイプを1,000万に変更しなければならなかったと言います。 これは長い時間で、仕事を残して仕事に出ました。

もちろん、並列リクエストの数を増やすことは可能ですが、他の誰かのIPを使用しているため、礼儀正しく振る舞う必要があります(そうでなければ、突然禁止されます)。



一般に、すべての家の識別子のリストがあります。次に、それらを処理し、利用可能なすべての情報を収集する必要があります。 このような配列は、手でテキストファイルにプッシュすることはできません。処理するのは不便です。 sqlite3を使用します。



データベースを作成します。 家と船体、構造などの追加要素を除いて、すべてのフィールドは明らかです。 たとえば、近くの家の傾向を見たい場合に備えて、取り壊します。



 import sqlite3 schema = "CREATE TABLE `houses` (\ `id` INTEGER PRIMARY KEY,\ `street` TEXT NOT NULL ,\ `house_nbr` TEXT NOT NULL,\ `house_additional` TEXT,\ `total_votes` INTEGER,\ `total_za` INTEGER,\ `meeting` INTEGER DEFAULT '0',\ `flats` INTEGER\ );" conn = sqlite3.connect('renovation.db') cur = conn.cursor() db = cur.execute(schema) conn.commit() conn.close()
      
      





まあ、ポイントまで! すべてのJSONエラー(おそらく、回答にJSONがなかった)と不明なエラー(ほとんど回答がなかった可能性が高い)を書き留める方法に沿って、家に関する情報を取得してデータベースに追加する関数を作成します。もちろん、もしあれば、別々に。



 import requests import re import sqlite3 def gethouseinfo(idd): print(idd) urly = 'https://www.mos.ru/altmosprx/api/1/renovation/house_result/' + str(idd) + '/' try: r = requests.get(urly) r.encoding = 'utf-8' results = r.json() adress = results['result']['address'] print(adress) if re.match('(.*), (.*), (.*)', adress): adress_street = re.match('(.*), (.*), (.*)', adress).group(1) adress_house = re.match('(.*), (.*), (.*)', adress).group(2) adress_building = re.match('(.*), (.*), (.*)', adress).group(3) else: adress_street = re.match('(.*), (.*)', adress).group(1) adress_house = re.match('(.*), (.*)', adress).group(2) adress_building = '' totalvotes = len(re.findall('apartment-id', results['result']['table'])) + len(re.findall('apartment-id', results['result']['und_table'])) aye = results['result']['total']['za'] meetinghappened = bool(results['result']['total']['protocol_res']) iddlist = [] iddlist.append(idd) check = cur.execute('SELECT * FROM houses WHERE id=?', iddlist) res = check.fetchone() if res: print('already exists') else: insert = cur.execute('INSERT INTO houses (id, street, house_nbr, house_additional, total_votes, total_za, meeting) values (?, ?, ?, ?, ?, ?, ?)', [idd, adress_street, adress_house, adress_building, totalvotes, aye, meetinghappened]) print('added ' + str(idd)) except ValueError: print('no data for id '+ str(idd)) jsonerror.append(idd) except: print('unknown eggog') unknownerror.append(idd) jsonerror = [] unknownerror = [] with open('/home/deb/mosres.txt') as fc: mosres = fc.read().splitlines() conn = sqlite3.connect('/home/deb/renovation.db') cur = conn.cursor() for house in mosres: gethouseinfo(house) conn.commit() conn.close() if jsonerror: with open('/home/deb/jsonerror.txt', 'w') as f: for item in jsonerror: f.write('{}\n'.format(item)) if unknownerror: with open('/home/deb/unknownerror.txt', 'w') as f: for item in unknownerror: f.write('{}\n'.format(item))
      
      





だから、これは何かです。 現在、市長室のウェブサイト上の公開情報に基づいて作成された、改修の投票に関するデータベースがあります。 すでに掘り下げて(最終的に!)グラフィックを描くことができます!



すべての結果(賛否両論の割合を視覚的に確認するために小から大にソート、X-自宅、プレーヤー-の割合、緑の線-便宜上、克服する必要がある66%を切り捨てる):







賛成票の分布:







これはすべて良いことですが、外観に興味がありました。 そして、ここでは難しくなります。 実際には、住所にアパートの数を備えた通常の、単純な、集中化されたリソースはありません。 少なくとも2GISには、少なくとも有料情報のアップロードの例では、アパートの数は存在しますが、有料ですか? これは私たちの方法ではありません! 私たちは他の方法で行きます。



容赦ないグーグルとヤンデックスは、サイトtvoyadres.ru/domaを指しており、そこには、多くの場合、アパートに関する情報のある家があります。 しかし、それらを収集する方法は? 理想的には、まずデータのある家のリスト全体(少なくとも通り)を収集してから、市役所の形式で取得したデータベース内の住所をこのサイトの形式の住所に接続してから、それらを引き出す必要があります何と結びついたのか、アパート。 おそらく通りから始める価値はありますか? tvoyadres.ru/ulitsy-しかし、200ページのソート、各問題のスクレイピングと処理は非常に退屈です。 たぶん、あなたもここでいくつかのAPIを見つけることができますか?



街のページでは成功が待っていました。街のリストだけでなく、「More streets」ボタンもあります: tvoyadres.ru/moskovskaya-oblast/goroda/551







うん! タイプリクエスト

http://tvoyadres.ru/js/street.php?region=81&city=&count=2073&_=1499809159225





は、道路へのリンク(およびリンクへの識別子)を含む道路のリストを提供します。 さて、リクエストの文字が何を意味するのかを理解することは残っています。 もちろん、私たちは地域や都市にも触れません。 最後の理解できないものを削除して、 tvoyadres.ru / js / street.php?region = 81&city = Moscow&count = 2073のみを残してみましょう-結果は同じです。 OK、もう一度ボタンをクリックします。同じリクエストが送信されましたが、カウントは100少ないことがわかります。 このパラメーターを手動で試してみましょう。



0-エラーが返されます。 1-1つの通りが戻ります。 2-2つの通りが戻り、すでに1つを見ています。 100-100通りが返されました。 200-別の100通りが戻ってきています。 私たちは2073年に始めました-2173を試してみませんか? はい、これらは街のページに表示された最初の百通りです。 2174?



重大なエラー



SQL構文にエラーがあります。 行1で '-1、100'の近くで使用する正しい構文については、MySQLサーバーのバージョンに対応するマニュアルを確認してください。


おっと。 カウントはLIMIT SELECTクエリに送られ、制限には常に100行あるように見えますが、最初の行はカウント-2173として計算されます。ちなみに、かなり世俗的なようです-SQLインジェクションの作成方法がわかりませんでした。それは送信されませんが、計算され、そしてあなたがそれに数字を入れなければ、それは陳腐になります。 まあ。 結果があります。 さて、ここまでです。



すべてが通常よりも簡単です:



 def getstreets(num): r = requests.get(url + str(num)) results = r.json() result = results['string'] return(result) for i in range(1, 2272, 100): totalres += getstreets(i)
      
      







そして結果を書きます。 結果は、たくさんのhtmlコードのようになります。 一般に、すでにここでは正規表現よりも優れたものに進むことができますが、 タスクは非常に単純です-それにもかかわらず、私たちは彼らによってアイデンティティの人々と一緒に街を引き出し、それから私たちはキーで彼らの口述を作ります-街の名前。



 sids = re.findall('ulitsy\/(.*?)">(.*?)<\/a>', totalres) #sids = streets with ids streetsdict = {} for i in range(len(sids)): key = sids[i][1] value = sids[i][0] streetsdict[key] = value
      
      





しかし、それから、私たちは通りのページから家に帰らなければなりません、言い換えれば-たくさんのhtmlで作業します。 このような一連の正規表現を解析しようとすると、より高価になります。 したがって、 BeautifulSoupに精通してください。



関数を作成し、別の辞書に対して実行します。この場合、キーは既に識別子であり、値はアパートメントです。 ロジックは次のとおりです。アパートの名前ごとに、IDごとにIDを取得できます。アパートを取得できます。



 import re from bs4 import BeautifulSoup def gethouses(num): r = requests.get('http://tvoyadres.ru/moskovskaya-oblast/moskva/ulitsy/' + str(num) + '/') results = r.text soup = BeautifulSoup(results, 'html.parser') ul = soup.find("ul", {"class": "next"}) houses = [] try: for li in ul.find_all("li"): urly = li.a['href'] urly = re.search('doma\/(.*)\/', urly).group(1) houses.append([li.get_text(), urly]) return(houses) except: print('None') return('None') totalyres = {} for key in sids: num = sids[key] totalyres[num] = gethouses(num)
      
      





2つのことが同時に思い浮かびます。 まず、各通りにリストがあり、各値が家の名前とその識別子を含むリストであるディクテーションではなく、ディクテーションが添付されたディクテーションを作成する必要があります。 これは単純なループに変えることができます。注意を集中しませんが、そのようなdict totreを呼び出しましょう-はい、ファンタジーは最後に完全に私を残しました。 ごめんなさい



2つ目-そして各家のyurlには、識別子だけでなく、通りの音訳も含まれています!



 for key in totre: urlo = 'http://tvoyadres.ru/moskovskaya-oblast/moskva/ulitsy/' + key + '/' ra = requests.get(urlo) try: streetname = re.search('<ul class="next"><li><a href="\/moskovskaya-oblast\/moskva\/(.+?)\/doma\/', ra.text).group(1) totre[key]['streetname'] = streetname except: print(key)
      
      





そして今、私たちはほとんど最も困難に直面しています。 データベースにある市役所の番地と、住所録の番地を使用した識別子を結び付ける必要があります。 Difflibはpythonに組み込まれているため、これに役立ちます。 しかし、difflibにはほとんど望みがありません。頻度と類似性はもちろん良いので、ユーザーが確認する必要がありますが、愚かな間違いを避ける必要があります。 一般に、フォーマットを見ると、クラスとして文字noがない場所、道路名から「通り」という単語が削除されている場所、そしてこれを行うことに気付きます。



 conn = sqlite3.connect('renovation.db') cur = conn.cursor() streets = cur.execute('SELECT DISTINCT street FROM houses order by street asc') streeets = streets.fetchall() conn.close() exactmatches = {} keyslist = [] for key in sids.keys(): keyslist.append(key) def glue(maxres=3, freq=0.6): for each in streeets: eachnoyo = each[0].replace('', '') diffres = difflib.get_close_matches(eachnoyo, keyslist, maxres, freq) if each[0] not in exactmatches.keys(): if len(diffres) == 1: print(each[0] + ': ' + diffres[0]) notcompleted = False while notcompleted == False: inp = input('Correct? y/n ') if inp == 'y': notcompleted = True exactmatches[each[0]] = sids[diffres[0]] elif inp == 'n': notcompleted = True else: print('Incorrect input, try again') elif len(diffres) == 0: print('No matches for ' + each[0]) elif len(diffres) > 1: print(each[0] + ': ' + str(diffres)) notcompleted = False while notcompleted == False: inp = input('List number? Or n ') try: listnum = int(inp) except: listnum = None if inp == 'n': notcompleted = True elif listnum in range(0, len(diffres)): notcompleted = True exactmatches[each[0]] = sids[diffres[0]] else: print('Incorrect input, try again') with open('exactmatches.json', 'w') as f: json.dump(exactmatches, f, ensure_ascii=False)
      
      





コンソールに座って、結果を見て、ボタンを押します。 サイクル全体が終わったら、たとえば、glue(10、freq = 0.4)など、より多くのスペアパラメーターで関数を開始します。







私は、コケックで700通りのうち506通りに忍耐しました。私の意見では、素晴らしい結果であり、最も重要なことは、統計的に有意です(最も可能性が高い)。



そして今、あなたは家のために同じことをする必要があり、実際には、その数のアパートを取ります。 そして、データベースに入れます。



 conn = sqlite3.connect('renovation.db') cur = conn.cursor() allhouses = cur.execute('SELECT * FROM houses WHERE flats IS NULL ORDER BY id') allhousesres = allhouses.fetchall() url2 = 'http://tvoyadres.ru/moskovskaya-oblast/moskva/' def getnumberofflats(streetname, houseid): urlo = url2 + str(streetname) + '/doma/' + str(houseid) + '/' r = requests.get(urlo) results = r.text numbe = re.search('<span class="left"> <\/span> <span class="right">(\d*)<', results).group(1) return numbe def gluehousesnumbers(freq=3, ratio=0.6): for house in allhousesres: if house[1] in exactmatches.keys(): housenbr = house[2].replace('', '') if house[3]: housenbr = housenbr + ' ' + house[3] housenbr = housenbr.lower() diffres = difflib.get_close_matches(housenbr, totre[exactmatches[house[1]]].keys(), freq, ratio) if len(diffres) == 1: print(housenbr + ': ' + diffres[0]) notcompleted = False while notcompleted == False: inp = input('Correct? y/n ') if inp == 'y': notcompleted = True try: flatsnumber = getnumberofflats(totre[exactmatches[house[1]]]['streetname'], totre[exactmatches[house[1]]][diffres[0]]) insertion = cur.execute('UPDATE houses SET flats = ? WHERE id = ?', [flatsnumber, house[0]]) except: print('weird, no flat number for ' + str(house)) elif inp == 'n': notcompleted = True else: print('Incorrect input, try again') elif len(diffres) > 1: print(housenbr + ': ' + str(diffres)) notcompleted = False while notcompleted == False: inp = input('List number? Or n ') try: listnum = int(inp) except: listnum = None if inp == 'n': notcompleted = True elif listnum in range(0, len(diffres)): notcompleted = True try: flatsnumber = getnumberofflats(totre[exactmatches[house[1]]]['streetname'], totre[exactmatches[house[1]]][diffres[0]]) insertion = cur.execute('UPDATE houses SET flats = ? WHERE id = ?', [flatsnumber, house[0]]) except: print('weird, no flat number for ' + str(house)) else: print('Incorrect input, try again') conn.commit() conn.close()
      
      





楽しみ続けています。 主なことは、各関数の実行後に変更をコミットすることを忘れないことです。 データベースを常にいじらないように、接続を追加して関数自体にコミットしませんでした。



はい、それはかなり疲れます。 しかし、繰り返しますが、すべてをdifflibに委ねると、「35 'b」よりも「35」に近い「35b」を考慮するスクリーンショットのように、愚かな間違いが発生します。 もちろん、これはdifflibエラーではありませんが、正直なところ、完璧なリクエストを探すのにもっと時間を費やし、どこかでつまずくでしょう。 ユーザーの確認で最高の、より自信を持って。







合計:アパートの数は、約4,500戸のうち3,592戸です! 素晴らしい結果(あなたは自分を賞賛しません-誰も賞賛しません)。 しかし、もちろん、非常に多くの偶然の一致が確認された場合、エラーが発生します。







アパートメントのみと思われるよりも多くのアパートメントが投票した43のアパートメントを削除します。 偶然の一致を誤って確認したか、明らかに間違ったデータがあったかは明らかです。



さて、残りの部分を使えば、すでに楽しいことができます。 投票率は、明らかに有権者の数をアパートの数で割ったものとしてカウントされます。 グラフも明らかに描画されますが、唯一のことは、投票による各データポイントに対して、グラフを描画しても意味がないことです。 影付きの紙のように見えるので、投票データを切り上げて、このデータポイントの平均結果データを取得することをお勧めします。



最も近い整数に丸める:







最も近い5の倍数に丸めます。







一般に、80%の投票率の領域での一対のアンチパイクを除いて、または、さらに遠くを丸めて見ると、一般に30%、40%、および80%の領域で、投票率は結果に影響しませんでした。 驚くべき方法で100%の投票率が常に100%の結果をもたらさない限り。 そして、平均して、投票率は58.7%でした。



それは価値がありましたか? 私にとって、はい、私は多くを学びました。 そして読者のために? さて、読者にはデータベース自体を投稿します



このデータでもっと面白いことができるかもしれません。



All Articles