[Python筆記] 2020台灣選舉結果爬蟲:以不分區政黨票為例

1. 一樣先偷看中選會的小秘密

先打開中選會的網站,點到立法委員選舉下面的政黨得票數 (不分區及僑居國外國民立法委員)然後按 F12 查看小秘密^Q\^。

這時候就可以從 tag 裡面找到一個規律啦~然後如果又把每個縣市點開來,也會發現像是:

  • id=“folder1260”」就是「政黨得票數 (不分區及僑居國外國民立法委員)
  • id=“folder1261”」就是「臺北市
    • id=“item1262”」就是「松山區
    • id=“item1263”」就是「信義區
    • id=“item1264”」就是「大安區
    • id=“item1273”」就是「北投區
  • id=“folder1274”」就是「新北市
  • id=“folder1304”」就是「桃園市
  • id=“folder1648”」就是「嘉義市
  • 反正「id=“folder___”」就是「某某縣市」,而「id=“item___”」就是「鄉鎮市區」

接下來就開始實作囉。

一樣先import會用到的library

  • pandas: 處理Dataframe
  • os: 處理輸出的檔案路徑
  • html5lib, bs4, selenium, requests: 處理網頁跟爬蟲
import pandas
import html5lib
import os
from bs4 import BeautifulSoup
import requests
from selenium import webdriver

處理點擊(click)問題

如果直接打開中選會的那個網站齁,會發現他把旁邊的地區都縮起來了,只有用滑鼠去「點擊」那個+,才會展開來,所以我們要去模擬把它「點開來」。

main_website_url = 'https://www.cec.gov.tw/pc/zh_TW/L4/n00000000000000000.html'
# 這個就是中選會關於立法委員的網址

domain_prefix = 'https://www.cec.gov.tw/pc/zh_TW/'
# 補個網址前輟,等下會用到

driver = webdriver.Chrome('C:\selenium_driver_chrome\chromedriver.exe')
driver.get(main_website_url)
# 使用selenium去模擬chrome,記得先去下載chromedriver
# 解壓縮後習慣放到C:\selenium_driver_chrome\chromedriver.exe之下

driver.find_element_by_id('folder1260').click()
# 用find_element_by_id去尋找id='folder1260'的「政黨得票數 (不分區及僑居國外國民立法委員)」
# 然後用.click()模擬滑鼠去「點擊」「政黨得票數 (不分區及僑居國外國民立法委員)」

開撈網址

soup = BeautifulSoup(driver.page_source, 'lxml')

links = soup.select('div[id^=folder] a')
# select div標籤裡的id,開頭(^=就是尋找開頭)為folder的所有東西,並列出<a></a>

這個時候如果把links印出來,會發現長這樣又臭又長。我打算要抓javascript:clickOnNode()裡面的數字(像是1261, 1274, 1304等等),所以是從「第五列」到「倒數第十列」當中的「偶數行」。

$ links
> [<a>查詢項目 (立法委員)</a>,
 <a href='javascript:clickOnNode("1")'><img border="0" height="22" id="nodeIcon1" name="nodeIcon1" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a>候選人得票數</a>,
 <a href='javascript:clickOnNode("1260")'><img border="0" height="22" id="nodeIcon1260" name="nodeIcon1260" src="../images/ftv2mnode.gif" width="16"/></a>,
 <a href="../L4/n00000000000000000.html" id="itemTextLink1260" onclick='javascript:clickOnFolder("1260")' style="color: white; background-color: rgb(255, 127, 0);" target="_top">政黨得票數 (不分區及僑居國外國民立法委員)</a>,
 <a href='javascript:clickOnNode("1261")'><img border="0" height="22" id="nodeIcon1261" name="nodeIcon1261" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a href="../L4/n63000000000000000.html" id="itemTextLink1261" onclick='javascript:clickOnFolder("1261")' target="_top">臺北市</a>,
 <a href='javascript:clickOnNode("1274")'><img border="0" height="22" id="nodeIcon1274" name="nodeIcon1274" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a href="../L4/n65000000000000000.html" id="itemTextLink1274" onclick='javascript:clickOnFolder("1274")' target="_top">新北市</a>,
 <a href='javascript:clickOnNode("1304")'><img border="0" height="22" id="nodeIcon1304" name="nodeIcon1304" src="../images/ftv2pnode.gif" width="16"/></a>,
... 

所以補上[5:-10]跟取偶數列的[::2]就會跑出我們要的東西。

links = soup.select('div[id^=folder] a')[5:-10][::2]
$ links
> [<a href='javascript:clickOnNode("1261")'><img border="0" height="22" id="nodeIcon1261" name="nodeIcon1261" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a href='javascript:clickOnNode("1274")'><img border="0" height="22" id="nodeIcon1274" name="nodeIcon1274" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a href='javascript:clickOnNode("1304")'><img border="0" height="22" id="nodeIcon1304" name="nodeIcon1304" src="../images/ftv2pnode.gif" width="16"/></a>,
 <a href='javascript:clickOnNode("1318")'><img border="0" height="22" id="nodeIcon1318" name="nodeIcon1318" src="../images/ftv2pnode.gif" width="16"/></a>,
 ...

然後抓出 href 裡面的東西,就是javascript:clickOnNode(),數字的部分都是固定的四位數,所以是取「倒數第六個」到「倒數第二個」的部分,所以取出[-6:-2]的部分,把這些縣市的(folder=‘XXXX’的部份)放到country_code裡面。

country_code = []
# 縣市的代碼

for ele in links:
    country_code.append(ele.get('href')[-6:-2])
    # 用.get抓出href的部份,接著抓出數字
$ country_code # 檢查一下是否從1261的臺北市抓到1648的嘉義市
> 1261
1274
1304
1318
1348
1386
1425
1439
1458
1485
1499
1520
1539
1573
1586
1600
1617
1624
1631
1636
1644
1648

用跟上面一樣的方法抓出鄉鎮市區的網址

districts_website_url = []
# 用來存放所有鄉鎮市區的的網址

for ele in range(len(country_code)):
    driver.get(main_website_url)
    driver.find_element_by_id('folder' + country_code[ele]).click()
    # 跟上面一樣,去模擬點擊,把每個縣市下面的鄉鎮市區抓出來
    soup = BeautifulSoup(driver.page_source, 'lxml')
    links = soup.select('div[id^=item] a')
    # select div標籤裡的id,開頭(^=就是尋找開頭)為item的所有東西,並列出<a></a>
    for i in links:
        districts_website_url.append(domain_prefix + i.get('href')[3:])
        # 用.get抓出href的部份,接著抓出item後的數字,並組合成網頁

到這裡之後,如果有抓到全部鄉鎮市區的網址,就算成功一半了

$ website_url
> ['https://www.cec.gov.tw/pc/zh_TW/L4/n63000000100000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000200000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000300000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000400000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000500000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000600000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000700000000.html',
 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000800000000.html',
 ...

2. 對表格開刀

臺北市松山區的選舉結果為例,我們可以先讀出這個網頁

site = 'https://www.cec.gov.tw/pc/zh_TW/L4/n63000000100000000.html'
dfs = pandas.read_html(site)

判別哪個表格才是我們要的

$ len(dfs)
> 6

所以這個網站裡面齁,共有6個表格,可以一一用dfs[0], dfs[1], dfs[2], dfs[3], dfs[4], dfs[5]去找哪個才是我們要的。最後我覺得dfs[3]才是我要ㄉ。有fu!

$ dfs[3]
>   0   1   2   3
0   號次  政黨  得票數 得票率 %
1   1   合一行動聯盟  54  0.0430
2   2   中華統一促進黨 147 0.1170
3   3   親民黨 4947    3.9376
4   4   安定力量    1041    0.8286
5   5   台灣基進    2648    2.1077
6   6   時代力量    10187   8.1085
7   7   新黨  3284    2.6139
8   8   喜樂島聯盟   218 0.1735
9   9   中國國民黨   45629   36.3190
10  10  一邊一國行動黨 474 0.3773
11  11  勞動黨 63  0.0501
12  12  綠黨  4468    3.5564
13  13  宗教聯盟    122 0.0971
14  14  民主進步黨   36313   28.9038
15  15  台灣民眾黨   15352   12.2196
16  16  台灣維新    67  0.0533
17  17  台澎黨 29  0.0231
18  18  國會政黨聯盟  376 0.2993
19  19  台灣團結聯盟  215 0.1711
20  NaN NaN NaN NaN

再處裡表格ㄛ

votes = dfs[3]
# 第四個表格才是我們要ㄉ!

votes.columns = votes.loc[0]
# 把第0列,視為表格的欄名

votes = votes.drop([0,20])
# 刪掉沒用的第0和第20列

votes.reset_index(drop=True, inplace=True)
# 重設index,其實也可以最後再統一一起做

areacode = site.split('/')[6].split('.')[0]
# 這其實就是分割出網頁最後的幾碼,我稱它為areacode
# 像是 https://www.cec.gov.tw/pc/zh_TW/L4/n63000000100000000.html
# 就是要取出n63000000100000000

areaname = dfs[1].iloc[0,0][17:-6]
# 去抓出「臺北市松山區」這幾個字,就是每個行政區的名字

countryname = areaname[:3]
districtname = areaname[3:]
# 分割出「臺北市」和「松山區」

if site[-9:-5] == '0000':
    voteplace = countryname + districtname + '總票數'
else:
    voteplace = countryname + '第 ' + site[-9:-5] + ' 號投開票所'
    # 像是https://www.cec.gov.tw/pc/zh_TW/L4/n63000000100000000.html
    # 就是臺北市松山區得總的票數(n63000000100000000)
    # 如果是https://www.cec.gov.tw/pc/zh_TW/L4/n63000000100000573.html
    # 就是臺北市松山區第0573投票所(n63000000100000573)
    
votes['投開票所縣市'] = countryname
votes['投開票所鄉鎮市區'] = districtname
votes['投開票所'] = voteplace
votes['投開票所代號'] = areacode[1:]
# 再把這些資料補上去

然後檢查!

$ votes
>   號次  政黨  得票數 得票率 %   投開票所縣市  投開票所鄉鎮市區    投開票所    投開票所代號
0   1   合一行動聯盟  54  0.0430  臺北市 松山區 臺北市松山區總票數   63000000100000000
1   2   中華統一促進黨 147 0.1170  臺北市 松山區 臺北市松山區總票數   63000000100000000
2   3   親民黨 4947    3.9376  臺北市 松山區 臺北市松山區總票數   63000000100000000
3   4   安定力量    1041    0.8286  臺北市 松山區 臺北市松山區總票數   63000000100000000
4   5   台灣基進    2648    2.1077  臺北市 松山區 臺北市松山區總票數   63000000100000000
5   6   時代力量    10187   8.1085  臺北市 松山區 臺北市松山區總票數   63000000100000000
6   7   新黨  3284    2.6139  臺北市 松山區 臺北市松山區總票數   63000000100000000
7   8   喜樂島聯盟   218 0.1735  臺北市 松山區 臺北市松山區總票數   63000000100000000
8   9   中國國民黨   45629   36.3190 臺北市 松山區 臺北市松山區總票數   63000000100000000
9   10  一邊一國行動黨 474 0.3773  臺北市 松山區 臺北市松山區總票數   63000000100000000
10  11  勞動黨 63  0.0501  臺北市 松山區 臺北市松山區總票數   63000000100000000
11  12  綠黨  4468    3.5564  臺北市 松山區 臺北市松山區總票數   63000000100000000
12  13  宗教聯盟    122 0.0971  臺北市 松山區 臺北市松山區總票數   63000000100000000
13  14  民主進步黨   36313   28.9038 臺北市 松山區 臺北市松山區總票數   63000000100000000
14  15  台灣民眾黨   15352   12.2196 臺北市 松山區 臺北市松山區總票數   63000000100000000
15  16  台灣維新    67  0.0533  臺北市 松山區 臺北市松山區總票數   63000000100000000
16  17  台澎黨 29  0.0231  臺北市 松山區 臺北市松山區總票數   63000000100000000
17  18  國會政黨聯盟  376 0.2993  臺北市 松山區 臺北市松山區總票數   63000000100000000
18  19  台灣團結聯盟  215 0.1711  臺北市 松山區 臺北市松山區總票數   63000000100000000

然後把上面一整個寫成function,名為getpartyvotes(),讓他可以自動化處理每個網址。

3. 再區分成各個投開票所!

繼續挖小祕密齁。

會發現第幾投開票所咧,其實就藏在這個tag的value值裡面,當有人選取的時候,就會自動跳到這個網頁裡。所以在這裡,我們要抓個鄉鎮市區網址的概念也是一樣。

votes_result = []
# 用來放結果的

for ct in range(len(districts_website_url)):
    driver.get(districts_website_url[ct])
    soup = BeautifulSoup(driver.page_source, 'lxml')
    links = soup.select('option[value]')
    # select出option標籤裡的value齁,像是n63000000100000573.html這一個部份
    
    for ele in links:
        try:
            votes_result.append(getpartyvotes(domain_prefix + 'L4/' + ele.get('value')))
            # 把每個網址,帶入上面抓表格的function,嘗試得到向上面那樣的結果。
            # 然後用.append的方式把新的DataFrame併入到原本的DataFrame後面
        except:
            continue
            # 以下除錯用,如果想知道有哪些網站沒抓好,就把他們print出來。
            # print(domain_prefix + 'L4/' + ele.get('value'))
    print(ct+1, ' / ', len(districts_website_url), ' is DONE.')
    # 單純用來表示說完成幾個鄉鎮市區
    
votes_result = pandas.concat(votes_result)
votes_result.reset_index(drop = True, inplace = True)
votes_result = pandas.DataFrame(votes_result) 
# 整理表格、重新給定index、強制轉為DataFrame

到此,votes_result就是最終結果了,要進行分析的話可以再透過這個DataFrame處理。

附錄:完整 code

如果只需要code跑跑看,或是爬完的結果,可以到我的Github找找看。

import pandas
import html5lib
import os
from bs4 import BeautifulSoup
import requests
from selenium import webdriver

def getpartyvotes(url):
    site = url
    dfs = pandas.read_html(site)
    votes = dfs[3]
    votes.columns = votes.loc[0]
    votes = votes.drop([0,20])
    votes.reset_index(drop=True, inplace=True)
    areacode = site.split('/')[6].split('.')[0]
    areaname = dfs[1].iloc[0,0][17:-6]
    countryname = areaname[:3]
    districtname = areaname[3:]
    if site[-9:-5] == '0000':
        voteplace = countryname + districtname + '總票數'
    else:
        voteplace = countryname + '第 ' + site[-9:-5] + ' 號投開票所'
    votes['投開票所縣市'] = countryname
    votes['投開票所鄉鎮市區'] = districtname
    votes['投開票所'] = voteplace
    votes['投開票所代號'] = areacode[1:]
    return votes

main_website_url = 'https://www.cec.gov.tw/pc/zh_TW/L4/n00000000000000000.html'
domain_prefix = 'https://www.cec.gov.tw/pc/zh_TW/'
driver = webdriver.Chrome('C:\selenium_driver_chrome\chromedriver.exe')
driver.get(main_website_url)
driver.find_element_by_id('folder1260').click()
# 政黨得票數 (不分區及僑居國外國民立法委員)

soup = BeautifulSoup(driver.page_source, 'lxml')
links = soup.select('div[id^=folder] a')[5:-10][::2]
country_code = []

for ele in links:
    # print(ele.get('href')[-6:-2])
    country_code.append(ele.get('href')[-6:-2])

districts_website_url = []
driver.get(main_website_url)

for ele in range(len(country_code)):
    driver.get(main_website_url)
    # print('folder' + country_code[ele])  
    driver.find_element_by_id('folder' + country_code[ele]).click()
    soup = BeautifulSoup(driver.page_source, 'lxml')
    links = soup.select('div[id^=item] a')
    for i in links:
        districts_website_url.append(domain_prefix + i.get('href')[3:])

votes_result = []
print('')
print('******************************************')
print('*                                        *')
print('*  2020_Taiwan_Election_Results_Crawler  *')
print('* Party-list proportional representation *')
print('*            不分區立委政黨票             *')
print('*                                        *')
print('******************************************')
print('')
print('There are total ', len(districts_website_url), 'districts during 2020 Taiwan election.')
print('')

#for ct in range(len(districts_website_url)):
for ct in range(2):
    driver.get(districts_website_url[ct])
    soup = BeautifulSoup(driver.page_source, 'lxml')
    links = soup.select('option[value]')
    for ele in links:
        try:
            votes_result.append(getpartyvotes(domain_prefix + 'L4/' + ele.get('value')))
        except:
            continue
            # 以下除錯用,如果想知道有哪些網站沒抓好就print出來。
            # print(domain_prefix + 'L4/' + ele.get('value'))
    print(ct+1, ' / ', len(districts_website_url), ' is DONE.')
votes_result = pandas.concat(votes_result)
votes_result.reset_index(drop = True, inplace = True)
votes_result = pandas.DataFrame(votes_result)     

#print(votes_result)
print('')
print('SUCCESS, ready to output as a .csv file')
father_path = os.getcwd()
file_name = input("input the file name you wish: ")
path_csv = father_path + '\\' + file_name + '.csv'
votes_result.to_csv(path_csv, encoding = 'utf-8') # 編碼可以自行設定
print('')
print('Ouput Success, the file is in ' + path_csv)
print('')
print('Repository: https://github.com/rutw/2020_Taiwan_Election_Results_Crawler')