selenium專案-國民健康署癌症數據之讀取

 

網頁內容之讀取 (爬蟲程式crawler)

  • 閱讀網站已經是現代人每天必做的事,是否有程式界面可以自己閱讀網站的內容,篩選使用者有興趣的內容,進而操作網頁的動作呢?答案是肯定的,一般稱之為crawler爬蟲程式。
  • 做為crawler程式,其基本語言可以是javascripts或其他語言,當然也可以是python。
  • Python模組中的爬蟲程式一般使用requests來抓取頁面內容,也進而使用BeautifulSoup 進行內容的剖析,再以讀取結果內容進行程式設計,可以說是非常完整。
  • 類似的作業如自動下載與讀取交通量VD數據([[2022-10-14-get_VDtp]]1、[[2022-10-13-rd_sht3]]2)

直讀與行動(selenium)

  • 此處除了前述BS的解析之外,進一步介紹更為直覺的selenium來讀取網頁內容。

建立驅動程式

  • 首先一樣要先輸入模組,建立驅動程式,並且開啟網頁:
$ cat start.py
from selenium import webdriver
driver = webdriver.Firefox(executable_path="/usr/bin/geckodriver")
driver.get("http://www.python.org")
  • 若無錯誤,程式會開啟python的官方網站的首頁。
  • 小括弧的內容若是已經可以由$PATH環境變數可以找得到,就不必特別寫,系統還是找得到執行檔,寫出來是提醒當工作站firefox不能啟動時,可能是geckodriver的版本不對、或不存在或路徑找不到,需要進一步查證。
  • 除了linux上內設是firefox做為瀏覽器之外,selenium以可以接受chrome、safari與ie,以下為MS windows上使用chrome的驅動方式。
driver = webdriver.Chrome \ 
('C:/Users/4139/AppData/Local/Google/Chrome/Application/chromedriver_win32/chromedriver.exe')
  • 執行檔並不是chrome.exe而是chromedriver.exe,可以在google網站下載適合的版本。即使是在PC上,程式所在位置可以接受正斜線”/”。
  • Linux上selenium當然也可以執行chrome,但是chrome已經不支援CentOS6了,如果是CentOS6環境,只能使用firefox。
  • firefox對https格式的限制,然而在chrome是OK的。

字詞解析程式(parser)

  • 如同前述butifulSoupselenium也可以列出網頁所讀到的內容,其命名方式更為直覺,如下例:
for element in driver.find_elements_by_css_selector("header.main-header"):
    print(element.text)
(result=)
Search This Site
GO
Socialize

Python is a programming language that lets you work quickly
and integrate systems more effectively. Learn More
  • 如果使用firefox,按下右鍵選取”inspect elements(Q)”便會出現如下圖的分割畫面,firefox會將內容按scc選擇器類別予以分類,其中的header類即為所輸出所示的內容文字。
  • 由於python程式設計經常使用試誤法,需要掌握所有的內容,從中挑選後續要執行的對象,因此find_element…的過程便非常重要。

點擊、選單等動作

  • 啟動瀏覽器驅動程式之後,python就可以透過驅動程式控制網頁,一般操作網頁最重要的動作包括點擊、下拉選單等,
  • 捲軸是因為畫面不夠,因此使用者要看到其他頁面時進行捲軸上下拉動,而我們針對頁面要直行的對象內容已經有所掌握了,因此不需要拉動捲軸即可直接執行動作。
  • 以下為點擊ID、xpath與下拉選單(點擊與選擇)的副程式範例。
from selenium import webdriver
from selenium.webdriver.support.ui import Select

def clkid(ID):                                  #click the id
    button_element = driver.find_element_by_id(ID)
    button_element.click()
    return
def clkpath(ID):                                        #click the xpath (no use)
    button_element = driver.find_element_by_xpath(ID)
    button_element.click()
    return
def SelectByIDnValue(ID,v):                     #click and select by value
    select = Select(driver.find_element_by_id(ID))
    select.select_by_value(v)
    return
#enter the website
driver = webdriver.Chrome( \
'C:/Users/4139/AppData/Local/Google/Chrome/Application/chromedriver_win32/chromedriver.exe')
driver.get('https://cris.hpa.gov.tw/pagepub/Home.aspx?itemNo=cr.q.30')
clkid('WR1_2_cmdEnter')
  • 因此我們只要指定瀏覽器的驅動程式(driver=…),指定要瀏覽的網頁(國民健康署癌症登記線上互動系統:一般(年齡別)),將每個頁面要點擊的項目、選單的內容定義清楚(如下段說明),利用序列及迴圈的技巧,不斷呼叫這些副程式即可達成目標,只剩下最後輸出檔案要如何保存的問題。

點擊的項目、選單內容定義

  • 以下範例是國健署癌症登記線上互動系統、一般(年齡別)的6個網頁點選內容
    1. 第1頁點選癌症類別、
    2. 第2頁點選年代、
    3. 第3頁點選年齡、
    4. 第4頁點選地區、
    5. 第5頁點選報告的圖或表,
    6. 第6頁是處理檔案
  • val(vXXX)表示是點選的值,id表示點擊的對象,dXXX表示XXX的val與XXX_name對照dict,_n表示出現在第n頁的點選內容。
val_data=['1','2']                                      #occurence and motality
val_point=['A'] #,'B','C','D']                          #statistical point
val_sex=['1','2'] #['0','1','2','3']
dCancer = rdtxt('cancer.txt')
id_cancer=[i for i in dCancer]#??7種ç???
ID2=["WR1_1_Q_YearBeginII","WR1_1_Q_YearEndII","WR1_1_btnNext"]
va2=['1979','2013']#?~Lä?張表顯示
id_age=['WR1_1_Q_AgeGroupII_'+str(i) for i in xrange(1,18)] #??7?~K年齡å???
id_reg=['WR1_1_QP_AreaRegionII','WR1_1_Q_AreaCity','WR1_1_Q_AreaTown']
Area=['ROOT','REGION_N','REGION_C','REGION_S','REGION_E','REGION_X','REGION_F']
nam=['全國','北區','中區','南區','東區','金馬','國外']
dArea={}
for i in xrange(len(Area)): dArea.update({Area[i]:nam[i]})
dCity=rdtxt('city.txt')#?°å?å¸?...?¶ä?
City=[i for i in dCity]
dTown=rdtxt('region.txt')
Town=[i for i in dTown]
dRegion={}
for i in dArea:dRegion.update({i:dArea[i]})
for i in dCity:dRegion.update({i:dCity[i]})
for i in dTown:dRegion.update({i:dTown[i]})
val_reg=[[0 for i in xrange(389)] for j in xrange(3)]
for i in xrange(len(Area)):val_reg[0][i]=Area[i]
for i in xrange(len(City)):val_reg[1][i]=City[i]
for i in xrange(len(Town)):val_reg[2][i]=Town[i]
ID5=['WR1_1_Q_ReportKindII_2',"WR1_1_cmdQuery"]
ID6=["WR1_1_ReportViewer1_ctl01_ctl05_ctl00",'WR1_1_ReportViewer1_ctl01_ctl05_ctl01']
va6=["Excel"]
delCols=['Unnamed: 0','Unnamed: 1','Unnamed: 2','Unnamed: 3']
cols=['Data','Point','Sex','Cancer','Age','Region'] #constant cols
df=DataFrame()
path=7*[0]
path[0]='C:\\Users\\4139\\Downloads\\'
  • 點選內容可以由rdtxt之程式讀取。該程式先將頁面原始碼剪下另存成txt檔,再運用文字讀取的程式技巧將癌症種類、地區鄉鎮縣市等內容讀取出來。例如下列html段落,rdtxt.py將會從中讀取縣市區域中文名稱(nam)及id(WRF_1QP_AreaCityII??)之對照關係。
<div style="display: table-row;" id="DivAreaCity">
            <label for="WR1_1_QP_AreaCityII" class="CSS_STDH">選擇縣市區域:</label>
			<input class="CSS_STD" id="WR1_1_QP_AreaCityII_0" type="checkbox" name="WR1_1_QP_AreaCityII_0" value="CRA_31">
			<label class="CSS_STD" for="WR1_1_QP_AreaCityII_0">新北市</label>
			<input class="CSS_STD" id="WR1_1_QP_AreaCityII_1" type="checkbox" name="WR1_1_QP_AreaCityII_1" value="CRA_34">
			<label class="CSS_STD" for="WR1_1_QP_AreaCityII_1">宜蘭縣</label>
...
			<label class="CSS_STD" for="WR1_1_QP_AreaCityII_22">其他</label>
			
        </div>
  • rdtxt.py內容
# coding=utf-8
"""
This Routine is used to read the dictionary of ID:NAME in cancer online query website
The input string (may with path) is the file name which contain "for" ID's and names
The output result is the dictionary, which may used in selenium calls
"""
def rdtxt(fname):
    w=[]                            #container of words
    with  open(fname) as ftext:         #open file
        for line in ftext:                #read each line
            for i in line.split():      #using the space as separation character
                if i[0:3]=='for':       #only the words leaded by 'for' are saved
                    w.append(i)
    redunt=['for=',"</label>",'</td>','"',"'"] #these characters or strings must be cleaned
    for k in redunt:
        for i in xrange(len(w)): w[i]=w[i].replace(k,'')                #replace with ''
    for i in xrange(len(w)): w[i]=w[i].replace('>',",") # > must change to ',' for further split
    (WR,nam)=([],[])                    #containers declarations
    for i in xrange(len(w)):    #working for each words
        lst=[j for j in w[i].split(',')]        #split the ID and name
        if len(lst)>1:                  #the overall ID may have no following name
            WR.append(lst[0])   #first is the keys
            nam.append(lst[1])  #the latter is the vlues
#   print w[:15]
    d={}                                                #store the dictionary
    for i in xrange(len(WR)):
        d.update({WR[i]:nam[i]})
    return d

序列及迴圈技巧與副程式之呼叫

  • 整體下載的資料庫共有7層迴圈,
  • 為簡省工作,資料以原始數據下載不作年齡標準化,另行計算。
  • 且因鄉鎮區太多,僅下載關切地區的區別數據,其餘則以縣市或大區為範圍下載。
  • 年代數據一次下載所有年代,會同時出現在excel資料表內。癌症選取避開性別與性器官癌症之不合理組合,亦可有效減少迴圈之浪費。
  • 程式藍色字部份為檔案管理,詳後分述,其餘則為迴圈與點選動作之進行。
for vdata in val_data:#[0:1]:
  path[1]=path[0]+vdata+'\\'
  for vpoint in val_point:#[0:1]:
    path[2]=path[1]+vpoint+'\\'
    for vsex in val_sex:#[0:1]:
      path[3]=path[2]+vsex+'\\'
      for icancer in id_cancer:#[0:1]:
        if vsex=='1' and icancer[:len(icancer)-2] == 'WR1_1_ctl180':continue
        if vsex=='2' and icancer[:len(icancer)-2] == 'WR1_1_ctl183':continue
        if vsex=='2' and icancer[:len(icancer)-2] == 'WR1_1_ctl185':continue
        path[4]=path[3]+icancer+'\\'
        for iage in id_age:#[0:1]:
          path[5]=path[4]+iage+'\\'
          for iACT in xrange(3):
            for jACT in xrange(389):
              NameNew=path[5]+str(iACT)+str(jACT)+'.xls'
              if val_reg[iACT][jACT]>1 and not os.path.exists (NameNew):
                if not os.path.exists(NameNew):copy2('c.txt', NameNew)
                driver.get('https://cris.hpa.gov.tw/pagepub/ Home.aspx ?itemNo=cr.q.30')
#page1/5 data, point, sex, kind of cancer
                ID1=["WR1_1_Q_DataII","WR1_1_Q_PointII",  \ "WR1_1_Q_SexII",icancer,"WR1_1_btnNext"]
                va1=[vdata,vpoint,vsex]
                for i in xrange(3):SelectByIDnValue(ID1[i],va1[i])
                for i in xrange(3,5):clkid(ID1[i])
#page2/5 year
                for i in xrange(2):SelectByIDnValue(ID2[i],va2[i])
                for i in xrange(2,3):clkid(ID2[i])
#page3/5 age
                ID3=["WR1_1_Q_AgeKindZone",iage,"WR1_1_btnNext"]
                for i in xrange(3):clkid(ID3[i])
#page4/5 region
                ID4=[id_reg[iACT],val_reg[iACT][jACT], "WR1_1_btnNext"]
                if iACT==0: #in case of all nation
                  SelectByIDnValue(ID4[0],ID4[1])
                else:
                  for i in xrange(2):clkid(ID4[i])
                clkid(ID4[2])
#page5/5 report type
                for i in xrange(2):clkid(ID5[i])
#result(page 6)
                SelectByIDnValue(ID6[0],va6[0])
                clkid(ID6[1])
                fname=path[0]+'Cr*.xls' #
                while not os.path.exists(fname):
                  time.sleep(1)
                  f=glob.glob(fname)
                  w=[x for x in subprocess.check_output( \
                  'dir/w '+fname, shell=True).split()]
                  if path[0]+w[7] in f:  fname=path[0]+w[7]
#                 while not os.path.exists(fname):time.sleep(1)
                copy2(fname, NameNew)
                os.remove(fname)
driver.close()
  • 點選另存檔案之後,chromedriver可以點選上一頁,此舉雖然可以減少跳出再進的時間,也不必再選擇身份,然而有很高的機率會出錯。因此還是以關掉驅動程式(.close())、重新啟動(.get(…))的方式,較為穩定不會出錯(紅字部分)。

檔案名稱與檔案管理

  • 下載檔案管理面臨之問題與解決方式

整理成一個大檔案或者儲存個別檔案?

  • 由於下載過程曠日費時,檔案另外處理,具有不怕斷線、容易累積的好處,而連線閱讀組成一超大資料庫,每次斷線後要重新讀取,並不符合經濟效益。
  • 網站產生檔案、chrome下載儲存檔案都需要時間,python無法等待,以致發生重複同一檔名,系統會增加(1)、(2)等字尾,予以辨識,增加未來檔案處理的困難度,。
  • 停等直到檔案出現的迴圈,如前述紫色字部分程式碼。如果系統給定的是確定的檔名(fname),可以用while not os.path.exists(fname)來判別(底線部分)。但由於不知道系統會給定什麼樣的檔案名稱,只知道字頭是Cr(Cancer Record),字尾是.xls,若運用wild card(*),not os.path.exists(fname)反而會得到True的結果,系統將會一直停等。
  • 因此必須運用2個方式檢查檔案名稱是否出現,一者為適合萬用卡的glob.glob()指令,一者為subprocess.check_output(‘dir/w ‘),前者會出現所有Cr*.xls,包括預設的CrXXX - 複製.xls檔案(glob若沒有檔案會發生錯誤),後者則利用中文檔名在DOS的dir/w指令中不會接副檔名的特性,排除在前者glob的結果之外,系統持續停等。
  • 當系統出現特定的CrReportN_A01_A.xls (或CrReportN_A01_B.xls…)檔案時,DOS的dir/w指令結果第7項為檔名,此時將明確檔名換掉萬用卡檔名,可以讓 os.path.exists(fname)生效,系統停止休息。 如此只有在明確的檔案名稱出現,二者對照成功,方能停止等待,繼續執行程式。

搬移或複製

  • 理論上,搬移(os.rename或os.renames)會比copy & remove更直接,但當目錄內已經有既有檔案時,rename將會失敗,此時就必須copy2。
  • 雖然確認檔案是否存在的邏輯判斷是在迴圈的前段就執行,此處仍然以可以覆蓋作為程式設計的原則,避免修正時的麻煩。

檔案個數的問題

  • 每次迴圈點選之後,不論是否有符合該點選條件的癌症發生率、致癌率,系統都會出一個查詢結果excel檔,因此如何將其由系統內定的檔名轉成特定檔名,且不會造成檔案個數太大無法正常運作檔案管理,必須有足夠多的目錄以及足以分辨的檔名系統。
  • 解決方式以5層目錄分開儲存這些檔案,而以迴圈的後2因子做為檔案名稱。目錄以累加方式產生,此一方式也可以用在後續的檔案管理。
for vdata in val_data:#[0:1]:
  path[1]=path[0]+vdata+'\\'
  for vpoint in val_point:#[0:1]:
    path[2]=path[1]+vpoint+'\\'
    for vsex in val_sex:#[0:1]:
      path[3]=path[2]+vsex+'\\'
      for icancer in id_cancer:#[0:1]:
        path[4]=path[3]+icancer+'\\'
        for iage in id_age:#[0:1]:
          path[5]=path[4]+iage+'\\'
          for iACT in xrange(3):
            for jACT in xrange(389):
              vals=[vdata,vpoint,vsex,icancer,iage,str(iACT),str(jACT)]
              lastname=path[5]+str(iACT)+str(jACT)
              # NameNew=path[0]
              # for i in xrange(7):NameNew=NameNew+vals[i]
              # NameNew=NameNew+'.xls'
              # if os.path.exists(NameNew): os.rename(NameNew,lastname)
              if os.path.exists(lastname+'xls'): os.rename(lastname+'xls',lastname+'.xls')

執行與記憶體管理

  • 由前述條件,當網站名稱為https時,將無法使用firefox或是工作站進行讀取,必須使用window上的chrome。而window上執行chromedrive,程式佔用系統的記憶體會緩慢增加。而當記憶體滿了的時候,chromedrive會crash當機,程式就中斷了。
  • 此時可以運用DOS的批次檔技巧予以解決,設計批次檔呼叫自己,就成了永不停止的批次檔,如下所示:
c:python select3.py
del C:\Users\4139\Downloads\CrReport*.xls
endless.bat
  • 當chrimedrive失效時,會釋放出記憶體,讓批次檔呼叫python繼續新的工作。因為程式內設計若目錄下若已經有要下載的檔案,就會跳過不執行,因此迴圈會從上次當機的地方繼續下載。
  1. https://sinotec2.github.io/FAQ/2022/10/14/get_VDtp.html “ 臺北市即時交通流量(VD)之下載與解讀” 

  2. https://sinotec2.github.io/FAQ/2022/10/13/rd_sht3.html “ 臺北市交通流量及特性(年度)調查數據檔案之讀取”