網頁擷取技巧

在所有的資料都放在網路上的時代,只要有合適的網址,就可以取得許多想要的資料,而這些資料有些以網頁的方式來顯示(如中央氣象局的氣象統計資料,Google的搜尋結果、奇摩電影時刻表等等)、有些是以DOC、CSV、PDF、ODS、或是XLS的方式儲存(如中央選取委員會的歷年統計資料、各級政府的公開資訊平台)、也有以JSON的方式提供的(如美國的地震觀測資料、各級政府的公開資訊平台)。不管是什麼型態的資料,在下載之前它們都只是一個網址,正確地說,是一個URL。 許多的網站只要有正確的網址,就可以不需要透過瀏覽器而取得網頁上所呈現的資料,例如奇摩的熱門新聞網址是https://tw.news.yahoo.com/most-popular,在瀏覽器看到的是以下這個畫面:

而中央氣象局的目前天氣預報如下:

但是當你在瀏覽器上按下滑鼠右鍵看到的,其實是它的原始資料,就是一堆密密麻麻的文字資料。但是不管如何,如果你打算有計畫地自動化利用程式讀取這些網頁資料, 瞭解網址的結構是首要之務!

網址解析

許多的網站資料之數量較大,要能夠有結構地找到不同網頁中所有我們想要的資料,瞭解網址組合是第一步。因為可能必須透過搜尋或是分頁的方式,才能夠取得需要的所有資料。以奇摩股市新聞為例,其網址如下:

在瀏覽了各個新聞網頁之後,仔細觀察其中網址的變化如下:

https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=4

此網址分別幾個部份,其中https是通訊協定,而tw.stock.yahoo.com則是網域名稱,/news_list/url/d/e/N1.html是網頁所在的位置和網頁檔案名稱,問號符號以後的q=&pg=4則是查詢用的參數,也是GET的參數。透過Python的urllib模組的urlparse分析函數,可以把這些參數內容都分開,如下所示的程式片段:

from urllib.parse import urlparse
u = urlparse("https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=4")
print(u.netloc)
print(u.path)
print(u.query)

執行結果如下:

tw.stock.yahoo.com
/news_list/url/d/e/N1.html
q=&pg=4

這個程式分別列出該網頁中的「網址:tw.stock.yahoo.com」、「網頁的路徑:/news_list/url/d/e/N1.html」、以及「查詢字串:q=&pg=4」。很明顯的,後面的pg=4就是目前網頁所在的頁數,而q=後面則是查詢字串,目前是沒有任何的查詢字串,因此預設就是不做任何查詢的操作,也就是顯示所有的資料。在抓取網頁資料的時候,如果像是上述的例子,同樣的資訊有超過一頁的內容需要擷取,只要分析網址的特色(也就是後面的查詢命令的規則和用法),抓取時再加以組合即可。而遇到比較複雜的網址,透過解析之後也可以比較瞭解如何自訂這些網址的參數。以奇摩股市要聞為例,它的查詢字串可以加上要查詢的關鍵字和頁數,透過以下的程式即可輕易地製作出前5頁的網址:

url = "https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg={}"
for i in range(1,6):
    print(url.format(i))

此程式的執行結果如下:

https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=1
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=2
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=3
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=4
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=5

同樣的觀察方式,在這個網站的各類型新聞瀏覽並檢視網址,讀者們應該就可以發現,N1.html的N1代表的是不同類別的新聞,利用變更這部份的資料,也可以透過網址直接取得不同類別的新聞資料。例如我們如果對於這個網站的重大要聞(N1)、最新新聞(N997)、以及科技產業(N4)有興趣,則可以利用以下的程式產生出這3類新聞的各前5頁的資料:

url = "https://tw.stock.yahoo.com/news_list/url/d/e/N{}.html?q=&pg={}"
for t in [1, 997, 4]:
    for i in range(1,6):
        print(url.format(t, i))

執行結果如下:

https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=1
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=2
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=3
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=4
https://tw.stock.yahoo.com/news_list/url/d/e/N1.html?q=&pg=5
https://tw.stock.yahoo.com/news_list/url/d/e/N997.html?q=&pg=1
https://tw.stock.yahoo.com/news_list/url/d/e/N997.html?q=&pg=2
https://tw.stock.yahoo.com/news_list/url/d/e/N997.html?q=&pg=3
https://tw.stock.yahoo.com/news_list/url/d/e/N997.html?q=&pg=4
https://tw.stock.yahoo.com/news_list/url/d/e/N997.html?q=&pg=5
https://tw.stock.yahoo.com/news_list/url/d/e/N4.html?q=&pg=1
https://tw.stock.yahoo.com/news_list/url/d/e/N4.html?q=&pg=2
https://tw.stock.yahoo.com/news_list/url/d/e/N4.html?q=&pg=3
https://tw.stock.yahoo.com/news_list/url/d/e/N4.html?q=&pg=4
https://tw.stock.yahoo.com/news_list/url/d/e/N4.html?q=&pg=5

上面這些網址,就是你可以自動化下載網頁的對象。

使用requests模組下載網頁資料

有了前面的網址基礎知識,大部份的網站就可以依照我們的想法在網頁上呈現出需要的資訊。那麼我們如何利用Python程式來擷取這些網頁到程式中呢?只要透過模組requests就可以了。 這個模組並不是Python系統預設的模組,所以在使用之前,可能需要在你的系統中先執行過「pip install requests」或是「pip3 install requests」才行,不過如果你使用之前我們在前面幾堂課中所教的Anaconda安裝的話,就不需要另行安裝。在確定安裝完畢之後,接下來就可以在程式中使用requests.get指令讀取想要處理的網頁內容,requests的使用方法如下所示:

import requests
url = "https://tw.stock.yahoo.com/news_list/url/d/e/N4.html"
html = requests.get(url).text
print(html)

這個程式會把目標網頁的內容下載回來,並把它們的內容列印出來,而列印的內容其實就是我們在瀏覽器中看到的原始碼內容。但是當你仔細檢視這些原始檔案的時候會發現,現代的網頁中都被加上了許多密密麻麻的HTML標籤,甚至是許多的Javascript程式碼,而這些並不是我們感興趣的。 由於回傳的資料都是一些文字檔案,雖然看起來沒有什麼結構,但基本上就是一些字元所組成的字串資料,把它們視為字串資料,即可使用一些Python的語法來搜尋以及操作其中的內容。以博客來網路書店的暢銷書排行榜為例,電腦資訊類的30日排行榜的網站:

以下是這個網站的原始碼看起來的樣子:

假設我們想要查詢某一個關鍵字(在此以"Python"為例)在此排行榜中出現的次數,可以使用以下的程式碼達成:

import requests
url = "https://www.books.com.tw/web/sys_saletopb/books/19/?loc=P_0002_020"
html = requests.get(url).text
print(type(html))
print("Python這個字在排行榜中裡面出現了{}次".format(
    html.count("Python")+html.count("python")))

上述程式的執行結果看起來可能是以下這個樣子:

<class 'str'>
Python這個字在排行榜中裡面出現了54次

簡單修改一下程式,就可以變成可查詢任意關鍵字在排行榜中出現次數的交談式應用程式:

import requests
url = "https://www.books.com.tw/web/sys_saletopb/books/19/?loc=P_0002_020"
html = requests.get(url).text
keyword = input("請問你要查詢的字串(end to quit):")
while keyword != 'end':
    print("{}這個字在排行榜中裡面出現了{}次".format(
        keyword, html.count(keyword)))
    keyword = input("請問你要查詢的字串:")

執行的結果如下:

請問你要查詢的字串(end to quit):python
python這個字在排行榜中裡面出現了2次
請問你要查詢的字串:docker
docker這個字在排行榜中裡面出現了0次
請問你要查詢的字串:Python
Python這個字在排行榜中裡面出現了52次
請問你要查詢的字串:end

使用字串來處理擷取回來的網頁內容方法很簡單,但是如果想要深入地找出這些內容裡面的特定資料卻十分麻煩。例如,它既然是一個排行榜,那麼如果我想要找出所有的書籍名稱以及作者的話,使用字串處理並沒有簡單的方法,這時候就需要更進一步地依照資料中所編排的文件結構去分析拆解,那麼我們需要的就是赫赫有名的BeautifulSoup模組。

認識HTML網頁

網頁上的資料主要是由HTML(Hyper Text Markup Language)語言所構成,當初在設計HTML語言的目的是為了讓讓網頁上的文件內容可以使用較美觀的方式呈現在瀏覽器中,同時也加入了超連結的語法,讓不同的網頁之間可以透過URL的型式在相互之間建立連結的關係,使瀏覽者可以自由地在不同網站的網頁間參閱所需要的資料。

如同在第九堂課的表9-4-1中所說明的,HTML透過了各式各樣的標籤標註網頁中的資料項目,這些標籤有些是用來描述其所含括的資料內容要以什麼方式呈現(或是有結構上的意義,例如~代表各級不同重要性的標題,以及代表的是文字段落等),或是在存放在不同的檔案或是網站上的連結網頁或資源(例如以及等)。HTML檔案的基本結構如下:

<html>
<head>
<meta 文件屬性設定>
<title>
</title>
<script ...></script>
<link rel=stylesheet type="text/css" ...>
</head>
<body>
<h1>標題</h1>
<p class='選擇器' id='識別符號' style='css格式命令'>
內文段落
</p>
<table>
<tr><td>欄位1</td><td>欄位2</td></tr>
<tr><td>欄位1</td><td>欄位2</td></tr>
</table>
<img src=...>
<a href='...'>外部連結</a>
</body>
</html>

每一個由小於符號「<」和大於符號「>」(又被稱為角括號)所包圍住的字串叫做標記(tag,或稱為標籤,在本書中會交替使用),大部份的標記都是成對地出現,但是後面出現的標記則多使用了一個除號開頭,例如,少部份的標記因為要呈現的資訊可以透過自身的屬性即可完成,所以只要一個就好,例如,即是在目前的位置顯示伺服器上的images資料夾下的pic.png圖形檔案。

綜上所述,我們可以簡單地把HTML檔案想成是一個充滿著HTML標籤的文件檔案,而這些標籤有些是獨立的,有些則是有上下層關係的結構。一個標準的HTML檔案之樹狀結構看起來如下圖所示:

如果對於HTML標籤有所瞭解,就可以大致地瞭解感興趣的資料是在哪一個標籤中,在程式中只要能夠鎖定該標籤,把它從文件中找出就可以了。例如,想要找出這個檔案中所有的圖形檔連結,那麼就可以找出所有的標籤,如果要找的是檔案中所有的外部連結,則只要找出所有的標籤就可以了。

簡單的標記如、等等,大部份的情況只有標記本身,並沒有什麼屬性可以取得,最多就是完整的標記描述「內容」,或是取得其content(內容)。但是有些標記本身還有自有的屬性需要設定,例如「<img src='images/01.jpg' title='第一張圖' alt='台南風景' width='300'」,此標籤名稱是img,而src、title、alt、width等等都是此標記的屬性,可以另外處理,此外,現代愈來愈複雜的網頁內容,也讓網頁設計者替許多標記加上各式各樣的自訂屬性名稱,這也是現代瀏覽器允許前端工程式外加設定的,而這些外加的屬性,往往就是網頁分析取得特定資料的關鍵。

使用BeautifulSoup模組分析網頁

BeautifulSoup是一套協助程式設計師解析網頁結構的專案,它起始於2004年,目前最新的版本是4.7.1,官方網頁的網址是:

http://www.crummy.com/software/BeautifulSoup/

在官網中有詳細的使用說明,也有中文的版本可以瀏覽,不過相對來說中文版的說明文件之版本就比較舊一些。 這個模組不是以字串處理的方式對待HTML文件內容,而是以如圖10-3-1所示的方式,一開始先對HTML文件做結構上的剖析,之後提供使用者一些搜尋的函數,讓使用者以標準的方式去查詢目標文件中的內容,取得所需要的資料。BeautifulSoup在使用之前可能需要以 pip install bs4進行安裝,安裝完成之後,即可以如下所示的程式碼對於目標文件進行剖析,並提供查詢(假設html是已下載之HTML格式字串資料):

soup = BeautifulSoup(html, "lxml")

其中"lxml"是其中一種剖析HTML檔案的方法,其它的方法還包括html.parser以及html5lib等等,不同的方法各自有其不同的特色,對於一般的HTML檔案來說,lxml就足夠使用了。以下是標準的BeautifulSoup使用方法:

import requests
from bs4 import BeautifulSoup
url = "http://ai******2000.pixnet.net/blog/post/16062839"
html = requests.get(url).text
soup = BeautifulSoup(html, "lxml")
print(type(soup))
print(dir(soup))

在上面的程式中,先使用print(type(soup))列出傳回之soup變數的型態,就是一個bs4.BeautifulSoup的類別實例,而利用dir(soup)即可看到此實例變數所提供的屬性以及方法函數,執行結果如下:

<class 'bs4.BeautifulSoup'>
['ASCII_SPACES', 'DEFAULT_BUILDER_FEATURES', 'HTML_FORMATTERS', 'NO_PARSER_SPECIFIED_WARNING', 'ROOT_TAG_NAME', 'XML_FORMATTERS', '__bool__', '__call__', '__class__', '__contains__', '__copy__', '__delattr__', '__delitem__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_all_strings', '_attr_value_as_string', '_attribute_checker', '_check_markup_is_url', '_feed', '_find_all', '_find_one', '_formatter_for_name', '_is_xml', '_lastRecursiveChild', '_last_descendant', '_most_recent_element', '_popToTag', '_select_debug', '_selector_combinators', '_should_pretty_print', '_tag_name_matches_and', 'append', 'attribselect_re', 'attrs', 'builder', 'can_be_empty_element', 'childGenerator', 'children', 'clear', 'contains_replacement_characters', 'contents', 'currentTag', 'current_data', 'declared_html_encoding', 'decode', 'decode_contents', 'decompose', 'descendants', 'encode', 'encode_contents', 'endData', 'extract', 'fetchNextSiblings', 'fetchParents', 'fetchPrevious', 'fetchPreviousSiblings', 'find', 'findAll', 'findAllNext', 'findAllPrevious', 'findChild', 'findChildren', 'findNext', 'findNextSibling', 'findNextSiblings', 'findParent', 'findParents', 'findPrevious', 'findPreviousSibling', 'findPreviousSiblings', 'find_all', 'find_all_next', 'find_all_previous', 'find_next', 'find_next_sibling', 'find_next_siblings', 'find_parent', 'find_parents', 'find_previous', 'find_previous_sibling', 'find_previous_siblings', 'format_string', 'get', 'getText', 'get_attribute_list', 'get_text', 'handle_data', 'handle_endtag', 'handle_starttag', 'has_attr', 'has_key', 'hidden', 'index', 'insert', 'insert_after', 'insert_before', 'isSelfClosing', 'is_empty_element', 'is_xml', 'known_xml', 'markup', 'name', 'namespace', 'new_string', 'new_tag', 'next', 'nextGenerator', 'nextSibling', 'nextSiblingGenerator', 'next_element', 'next_elements', 'next_sibling', 'next_siblings', 'object_was_parsed', 'original_encoding', 'parent', 'parentGenerator', 'parents', 'parse_only', 'parserClass', 'parser_class', 'popTag', 'prefix', 'preserve_whitespace_tag_stack', 'preserve_whitespace_tags', 'prettify', 'previous', 'previousGenerator', 'previousSibling', 'previousSiblingGenerator', 'previous_element', 'previous_elements', 'previous_sibling', 'previous_siblings', 'pushTag', 'quoted_colon', 'recursiveChildGenerator', 'renderContents', 'replaceWith', 'replaceWithChildren', 'replace_with', 'replace_with_children', 'reset', 'select', 'select_one', 'setup', 'string', 'strings', 'stripped_strings', 'tagStack', 'tag_name_re', 'text', 'unwrap', 'wrap']

在這麼多支援的方法函數中,我們最常用的就是find_all函數,它可以協助我們找出所有的指定標籤或選擇器(selector,後面會再加以說明)。例如,有一個目標網頁,我們想要把此網頁中所有圖形檔案都列示出來,可以編寫如下所示的程式:

import requests
from bs4 import BeautifulSoup
url = "http://ai******2000.pixnet.net/blog/post/16062839"
html = requests.get(url).text
soup = BeautifulSoup(html, "lxml")
images = soup.find_all("img")
for image in images:
    print(image["src"])

程式在完成剖析之後,透過soup.find_all("img")即可找出在網頁中所有影像連結的標籤(),取出的images是一個串列格式的變數,利用for迴圈即可逐一取出每一個資料項目,也就是網頁中的每一個標籤,由於標籤中的src通常代表的就是實際圖檔的影像位置,因此執行結果就是所有圖形檔案的網址列表,如下所示:

https://d5nxst8fruw4z.cloudfront.net/atrk.gif?account=H00Mh1aIE700wg
http://www.blogad.com.tw/Transfer/TrackH.aspx?BM_ID=234132&M=354
https://pic.pimg.tw/******2000/1329322203-3003024141.jpg
https://pic.pimg.tw/******2000/1329322046-2510769647.jpg
https://pic.pimg.tw/******2000/1329322047-876789942.jpg
https://pic.pimg.tw/******2000/1329322049-4213371248.jpg
https://pic.pimg.tw/******2000/1329322051-1388702738.jpg
https://pic.pimg.tw/******2000/1329322052-2302585952.jpg
https://pic.pimg.tw/******2000/1329322053-1213280226.jpg
https://pic.pimg.tw/******2000/1329322054-1862151904.jpg
https://pic.pimg.tw/******2000/1329322056-1692678013.jpg
https://pic.pimg.tw/******2000/1329322057-1918093159.jpg
https://pic.pimg.tw/******2000/1329322059-88415372.jpg
https://pic.pimg.tw/******2000/1329322062-2790795786.jpg
https://pic.pimg.tw/******2000/1329322063-3451628136.jpg
https://pic.pimg.tw/******2000/1329322065-344278763.jpg
https://pic.pimg.tw/******2000/1329322066-773459777.jpg
https://pic.pimg.tw/******2000/1329322068-1082077017.jpg
https://pic.pimg.tw/******2000/1329322069-366313511.jpg
(以下省略)

在jupyter notebook介面中即可直接點擊每連結以檢視該圖形檔案。有了這些連結,即可輕易地利用檔案操作指令把每一個圖形檔案都儲存在自己的電腦中,以下是其中的一種方法:

import requests
import os
from os.path import basename
from bs4 import BeautifulSoup
import urllib.request
url = "http://******2000.pixnet.net/blog/post/16062839"
html = requests.get(url).text
soup = BeautifulSoup(html, "lxml")
images = soup.find_all("img")
if not os.path.exists("images"):
    os.mkdir("images")
for image in images:
    image_url = image["src"]
    if ".jpg" in image_url:
        image_filename = basename(image_url)
        with open(os.path.join("images", image_filename), "wb") as fp:
            image_data = urllib.request.urlopen(image_url).read()
            fp.write(image_data)
        print(image_url)
        print(image_filename)

擷取網頁上的新聞訊息

假設我們想要把某一個網站上的新聞標題擷取下來,除了需要知道該網站的網址之外,瞭解新聞頁面網址的編碼方式以及我們所感興趣的新聞標題之HTML標籤安排也是需要瞭解的地方,以國立高雄科技大學的焦點新聞為例,剖析的程式如下(為了避免程式被濫用,網址部份有做了修改,正確的網址老師會在上課的時候說明):

import requests
from bs4 import BeautifulSoup
import time, random
url = 'https://www.nkust.edu.tw/*/***-****-**-{}.php?Lang=zh-tw'
for p in range(1, 20):
    html = requests.get(url.format(p)).text
    soup = BeautifulSoup(html, 'lxml')
    news = soup.select('#Dyn_2_2 > div')
    for item in news[0].select('#pageptlist > div > div > div > div > h5'):
        print(item.select('i')[0].text.strip(), end='')
        print(item.select('a')[0].text.strip())
    time.sleep(random.randint(1,3))

上述的程式執行結果如下(摘要部份內容):

2019-05-03走讀原鄉X體驗部落 高科大博雅教育中心辦原民文化體驗工作坊
2019-05-02高科大資源再生利用 翻轉海洋廢棄物價值
2019-05-02高科大千人大會師烤肉聯誼活動 海內外校友齊聚聯繫情感
2019-04-30地震、豪雨 即時監測結構物傾斜狀態 高科大營建系守護偏鄉小學安全
2019-04-262019南面而歌校園講座高科大登場 流行音樂產業重量級業師齊聚燕巢校區
2019-04-25高科大攜手澎湖海洋公民科學家 建立珊瑚礁區健康監測及海底覆網回報機制
2019-04-19高科大創設系「人之初」畢業展 11件作品入圍2019金點新秀設計獎
2019-04-18台灣微軟攜手高科大BIS學院 開啟管理學院轉型新典範
2019-04-17生鮮小達人登場!高科大攜手全聯 推廣在地水產品
2019-04-16打造低碳環境!4/21高科大環境安全衛生暨消防工程研討會 科工館登場
2019-04-122019高雄自動化工業展 高科大秀18項教師研發技術 打造產業第一的校園環境
2019-04-11知名企業家侯西泉蒞高科大演講 分享經營過程與人生智慧
2019-04-09高科大建置微電商平台 微電商達人培訓營助農漁產品行銷
...以下省略...

程式的重點在第8行和第9行,你要知道select()函數中的那些編碼是如何取得的,這就是從網頁中取出資料的關鍵。

下 面這個例子是取出某新聞網站的即時新聞標題以及網址的方法:

from bs4 import BeautifulSoup
import requests
url = 'https://***.com/news/*******/*'
html = requests.get(url).text
soup = BeautifulSoup(html, 'lxml')
news = soup.select('#breaknews_body > dl > dt > h2 > a')
for title in news:
    print(title.text)
    print("https://***.com"+title['href'])

以下這個程式則是取出某一線上書店的即時暢銷排行榜的所有書籍名稱:

url = 'https://********.com.tw/books/v2/salesrank&region=DJAA&offset={}&limit=10&_callback=jsonpcb_salesrank&25950594'
import requests
import json
import re

rank = 1
for p in range(1, 100, 10):
    html = requests.get(url.format(p)).text
    js = re.search('jsonpcb_salesrank\((.*?)\);', html)
    data = json.loads(js.group(1))
    for item in data['Item']:
        print(rank, item['Name'])
        rank += 1
    print("-----")

Last updated