【教學工作坊】「網路爬蟲」製成術

Pyladies Taiwan

Speaker : Mars

2016/10/29

Roadmap

  • Python 簡介
  • 什麼是網路爬蟲、網路爬蟲可以做什麼?
  • jupyter使用簡介
  • 第一隻網路爬蟲
  • 變數
  • 檔案輸出
  • HTML 解析
  • 迴圈

Python 簡介

  • 簡潔易懂
  • 要求程式碼寫作風格
  • 可以做很多事情

應用

  • [網路爬蟲]:urllib、requests、lxml、beautiful_soup、scrapy、selenium
  • [資料庫串接]:sqlite3(sqlite)、MySQLdb(MySQL)、pymssql(MSSQL)、Psycopg(PostgreSQL)
  • [自然語言]:NLTK、jieba
  • [統計應用]:pandas、numpy、scipy、matplotlib
  • [機器學習]:scikit-learn、TensorFlow
  • [影像處理]:PIL、opencv
  • [網站架設]:Django、Flask
  • [網路分析]:scapy
  • [GUI設計]:tkinter、PyQt
  • [軟硬整合]:raspberry pi 樹莓派、Arduino
  • [遊戲開發]:pygame
  • [App開發]:kivy
  • [各種服務的API串接]:Bot

程式設計 Roadmap

  • [基礎學習]:變數型態、判斷式、迴圈、函式、類別、模組、檔案IO、例外處理
  • [進階技巧]:Effective Python
  • [各種應用]

什麼是網路爬蟲、網路爬蟲可以做什麼?

程式設計

幫我們自動化處理事情,減少重複的動作

網路爬蟲

幫我們自動化從網頁上去擷取我們所需要的資料,減少重複的動作

  • 點擊網頁 or 輸入查詢資料
  • 等待網頁開啟
  • 選擇「想要」的內容
  • 存到另外的地方
  • 重覆以上動作

網路爬蟲的應用

  • 截圖(jpg,pdf...)
  • 每天的股票訊息
  • 即時外幣匯率
  • 神奇寶貝出沒通知
  • 查詢轉乘最優惠的機票:skiplagged
  • 圖庫/影片下載器
  • 查詢哪家店面賣的商品最便宜
  • 比對網軍

進階-資料分析

Roadmap

基礎

  • 抓取網頁:requests
  • Python 基礎:判斷式、迴圈、資料架構、函式
  • 解析內文:lxml
  • 編碼處理:big5、unicode、utf8
  • 資料正規化:string,re,clean_tag
  • 檔案存取與格式介紹:file、sys、json、csv
  • 資料庫存取:sqlite3 (MySQLDB)
  • 好用工具介紹
  • 實戰解析
  • 其他學習資源

進階

  • 瀏覽器模擬:selenium+PhantomJS
  • 驗證碼處理:pytesseract、2captcha
  • 效率改善:multi-processing
  • 框架:Scrapy
  • 環境建置、定期排程

jupyter簡介

結合編輯環境、編譯器、Terminal、檔案總管、工作管理員於網頁介面

在開始寫第一隻網路爬蟲之前,
我們先來介紹我們的開發環境:jupyter(ipython notebook)

為了避免大家在第一次上手,
用不同電腦、不同作業系統下,可能會遇到各種安裝環境上的問題,
我們架設了伺服器,讓大家可以直接在網站上使用 jupyter (ipython notebook)

Workshop結束後,如何自己安裝?

jupyter 環境安裝

登入 Pyladies Server

jupyter 介面

  • Files:檔案總管
    • New
      • Text Files
      • Folder
      • Terminal
      • Notebooks
    • Upload
    • Delete (選擇檔案)
    • Rename (選擇檔案)

  • Running:執行中程式管理,可以把沒用到的Notebook、Terminal關閉。

Terminal

在Teminal介面,可以執行許多Unix語法:

修改密碼

  • 新增一個Terminal (New>Terminal)
  • 輸入指令 passwd , 按下 Enter,輸入新密碼

檔案管理

除了用jupyter的檔案總管介面管理檔案,也可以利用Terminal執行Unix語法做檔案管理。

  • cd <path>:進入位置
  • ls <path>:列出位置下的檔案
  • mkdir <folder_name>:新增檔案夾
  • cp <now_file_path> <new_file_path>:複製檔案
  • mv <now_file_path> <new_file_path>:移動檔案
  • rm <now_file_path>:刪除檔案 -rm -r <now_path>:刪除檔案夾
  • pwd:顯示現在的位置

Notebooks

使用jupyter撰寫&執行 第一隻Python 程式

  • 新增一個Notebook (New > Notebooks | Python3)
  • 輸入程式碼 print ("Hello PyLadies"),按下介面上的 或是用快捷鍵Ctrl+EnterShift+Enter編譯執行

編輯與指令模式

  • 編輯模式(綠),更改cell內容
    • 滑鼠雙擊cell編輯區左方空白
    • Enter
  • 指令模式(藍),快捷鍵執行指令
    • 滑鼠單擊cell編輯區左方空白
    • Esc

!注意

  • 快捷鍵並無大小寫之分

cell 操作

  • 在下方插入新的cell
    • 介面Insert選單、
    • B
  • 在上方插入新的cell
    • 介面Insert選單
    • A
  • 刪除
    • 介面Edit選單、
    • DD
  • 複製
    • 介面Edit選單、
    • C
  • 在此cell下貼上
    • 介面Edit選單、
    • V
  • 將此cell往上/下移動
    • 介面Edit選單、 /

cell 類型

  • Code,程式碼撰寫及執行
    • 介面Cell選單
    • Y
  • Markdown,可做筆記用途 Markdown語法介紹
    • 介面Cell選單
    • M

編譯執行

  • 編譯執行後保持原本cell
    • 介面Cell選單
    • Ctrl+Enter
  • 編譯執行後跳向下方cell
    • 介面Cell選單、
    • Shift+Enter

!注意

  • 如果有用到上方cell的程式碼內容,需要先編譯執行上方cell的程式碼
  • 如果針對現在的cell多次編譯執行,並不會影響到上方cell已編譯執行後的結果

第一隻網路爬蟲

網頁的運作原理

利用 Chrome 的 「開發人員工具」觀察

  • 對著網頁按右鍵 > 檢查 > Network
  • 功能列表 > 更多工具 > 開發人員工具

http://blog.marsw.tw 為例

  • 這個網頁 傳遞資料的方法為 GET

Target:抓取網頁,並取出想要的資料!

  • 取得文章類別
  • 取得文章標題
  • ......etc.
In [21]:
from lxml import etree
import requests

url = "http://blog.marsw.tw"
response = requests.get(url)
html = response.text

fileout = open("test.html","w")
fileout.write(html)
fileout.close()

page = etree.HTML(html)
first_article_tags = page.xpath("//footer/a/text()")[0]
print (first_article_tags)

for article_title in page.xpath("//h5/a/text()"):
    print (article_title)
旅遊-日本
2014。09~關西9天滾出趣體驗
2014。05 日本 ~ Day9 旅程的最後~滾出趣精神、令人屏息的司馬遼太郎紀念館
2014。05 日本 ~ 滾出趣(調查11) 道頓堀的街頭運動
2014。05 日本 ~ Day8 鞍馬天氣晴、貴船川床流水涼麵、京都風情
2014。05 日本 ~ 高瀬川、鴨川、祇園之美

變數

  • 資料儲存的空間
from lxml import etree

只從 lxml 工具箱,裝備 etree 這個工具

import requests

裝備 requests 工具箱

url = "http://blog.marsw.tw"

"http://blog.marsw.tw" 這個 <字串 string>
儲存到 我們命名的 url <變數 variable>

response = requests.get(url)

使用 requests 工具箱的 get 工具,
這個工具能幫我們抓下網址為 url 的網頁(傳遞資料的方法為 GET )上的資料,
把這些資料存到 我們命名的 response <變數 variable>

html = response.text

response 屬於 text 的資料,也就是網頁原始碼(response是用get抓下來的網頁資料)
存到 我們命名的 html <變數 variable>

工具箱、工具 在 Python裡面的專有名詞分別是
「模組 module」和 「函式 function / 類別 class」,
比較複雜一點所以今天不會特別說明。

我們先來看看 <變數 variable>以及 <字串 string>

變數 (variable)

  • 變數是一個容器,是資料的儲存空間
  • 在程式中「=」是「賦值」的意思:將右邊的結果儲存到左邊的變數
    url = "http://blog.marsw.tw"
    response = requests.get(url)
    html = response.text
    
  • urlresponsehtml 都是自行命名的變數
  • 可以命名的字元:_、0~9、a~Z

特別注意!!!

  • 不能以數字開頭
  • 不能與保留字相同(ex: import, for, in, str...)
  • 大小寫有別
  • 好的命名方式會讓程式碼容易閱讀
    • xyz = "http://blog.marsw.tw" vs.
      url = "http://blog.marsw.tw"

字串(string)

  • 用「成對的」雙引號"或是單引號',將字串包住
  • 可以直接給定詞句
  • 也可以給空白句
  • 可以相+ (但不能相-)
  • 可以彈性填入詞句:使用「%s
In [238]:
my_string = "PyLadies Taiwan"
my_string2 = ""
my_string2 = my_string2 + "Py" + "Ladies"
my_string3 = "Py%s Taiwan"%("Ladies")

print (my_string)
print (my_string2)
print (my_string3)
PyLadies Taiwan
PyLadies
PyLadies Taiwan

格式化字串 %s 應用情境

In [1]:
# 產生各股票網址(股票代號是在網址中,不是在尾端)
url = "http://www.wantgoo.com/stock/%s?searchType=stocks"%(2330)
print (url)
url = "http://www.wantgoo.com/stock/%s?searchType=stocks"%(2371)
print (url)
http://www.wantgoo.com/stock/2330?searchType=stocks
http://www.wantgoo.com/stock/2371?searchType=stocks
In [12]:
# 產生facebook粉絲頁資訊(想讓資訊好看,不想用+來處理)
url = "https://www.facebook.com/%s/%s/"%("pyladies.tw","about")
print (url)
url = "https://www.facebook.com/%s/%s/"%("pyladies.tw","photos")
print (url)
https://www.facebook.com/pyladies.tw/about/
https://www.facebook.com/pyladies.tw/photos/

不使用格式化字串,直接用字串相加就會變成:

url = "https://www.facebook.com/"+"pyladies.tw"+"/"+"about"+"/"

而不用變數 url 儲存,直接用print印出,就會像以下看起來很雜亂的程式碼:

print ("https://www.facebook.com/"+"pyladies.tw"+"/"+"about"+"/")

檔案輸出

  • 先把檔案存起來,之後要拿來使用的時候就不需要再重爬
    • 網路太慢、沒有網路
    • 網頁的資料量太多
    • 方便用我們習慣的軟體開啟瀏覽
fileout = open("test.html","w")

open是讓我們開啟一個檔案的工具,而這個檔案名稱我們叫做test.html
而這個檔案是用來「寫入資料」,因此要加上w,不加的話會是用來「讀取檔案」。
我們把這個工具存在 fileout這個我們命名的變數中。

fileout.write(html)

然後使用 write 功能將 存在變數 html 的資料寫入(會寫在 test.html 中)

fileout.close()

最後,怕同時間有其他人/程式也一起使用這個檔案,
會造成檔案的內容被影響,所以我們在程式中以close功能關閉這個檔案,
至少現階段這個程式不會再影響檔案了。

In [8]:
article = "Bubble tea represents the 'QQ' food texture that Taiwanese love. The phrase refers to something that is especially chewy, like the tapioca balls that form the 'bubbles' in bubble tea. It's said this unusual drink was invented out of boredom. Chun Shui Tang and Hanlin Tea Room both claim to have invented bubble tea by combining sweetened tapioca pudding (a popular Taiwanese dessert) with tea. Regardless of which shop did it first, today the city is filled with bubble tea joints. Variations on the theme include taro-flavored tea, jasmine tea and coffee, served cold or hot."
fileout = open("my_article.txt","w")
fileout.write(article)
fileout.close()

! 注意

為什麼要用fileout這個變數來儲存open的檔案,
因為我們是用w來寫檔案,遇到一模一樣名字的檔案,會直接把原有的資料洗掉,

所以如果寫成:

open('test.html','w').write(html)
open('test.html','w').close()

第二次用 openw 開啟 test.html檔案,
就把第一次 write 的內容洗掉了,
因此我們使用變數,讓我們開一次檔案就好。

Coding Time

現在大家知道了

  • 變數
    • 字串 string
  • 檔案輸出
  • 抓下 傳遞資料的方法為 GET 的網頁

輸入程式碼,按下介面上的 或是用快捷鍵Ctrl+EnterShift+Enter編譯執行

In [ ]:
from lxml import etree
import requests

url = "http://blog.marsw.tw"
response = requests.get(url)
html = response.text

fileout = open("test.html","w")
fileout.write(html)
fileout.close()

在跟程式同一個目錄底下,會看到一個test.html的檔案

開啟檔案後,點選Download可以下載檔案

下載後以瀏覽器開啟,會發覺用程式抓下來的就是跟網址上一模一樣的網頁

更換 url 來抓下不同的網頁看看吧!

You can try

  • 變數名稱可以自行更改
In [ ]:
from lxml import etree
import requests

url = "http://blog.marsw.tw"
response = requests.get(url)
html = response.text

fileout = open("test.html","w")
fileout.write(html)
fileout.close()

好用 Chrome 擴充功能

HTML 解析

page = etree.HTML(html)

把變數html儲存的資料(網頁抓下來的原始碼),
以 工具etreeHTML功能,轉換成「XPath的節點(node)型態」,
並儲存到變數page中。

範例程式第一行宣告的 lxml 工具箱的 etree 工具,終於要用到啦!

from lxml import etree

HTML(Hyper Text Markup Language)

  • 右鍵 > 檢視網頁原始碼
  • 是用來描述網頁的一種標記語言 (markup language)
  • 由一堆標記標籤(markup tag)所構成

常見標籤(tag)

  • 連結<a href="">連結文字</a>
  • 圖片<img src=""/>
  • 內文標題<h1>~<h6>
  • 換行<br>

範例網頁

<html>
<head>
  <title>Title</title>
</head>
<body>
  <h1>Subtitle</h1>
  <a href="http://tw.pyladies.com/">PyLadies Website</a>
  <p>
      This is a paragraph <br>
      <a href="http://www.meetup.com/PyLadiesTW/">PyLadies Meetup</a> <br>
      <a href="https://www.facebook.com/pyladies.tw">PyLadies FB</a> <br>
  </p>
  <img src="http://tw.pyladies.com/img/logo2.png" width="99px"/>
</body>
</html>
first_article_tags = page.xpath("//footer/a/text()")[0]

前面的程式碼,我們已經讓變數page是「XPath的節點(node)型態」,

這邊的程式碼是以變數pagexpath找尋
整個文件中 // , 所有 footer 標籤,且小孩是 a 標籤的文字屬性 text()
而我們只取「第1個」結果,存到變數 first_article_tags 中。

print (first_article_tags)

將 變數 first_article_tags儲存的資料,印到螢幕上。

XPath

In [7]:
from lxml import etree

# 這邊先不使用request去抓,直接將原始碼存到 變數 html 中,方便大家理解
# 遇到長篇文章有換行的存在,可用「三個」雙引號或單引號
html = """
<html>
<head>
  <title>Title</title>
</head>
<body>
  <h1>Subtitle</h1>
  <a href="http://tw.pyladies.com/">PyLadies Website</a>
  <p>
      This is a paragraph <br>
      <a href="http://www.meetup.com/PyLadiesTW/">PyLadies Meetup</a> <br>
      <a href="https://www.facebook.com/pyladies.tw">PyLadies FB</a> <br>
  </p>
  <img src="http://tw.pyladies.com/img/logo2.png" width="99px"/>
</body>
</html>
"""
page = etree.HTML(html)
link_text_list = page.xpath("//a/text()")
link_text_p_list = page.xpath("//p/a/text()")
link_list = page.xpath("//a/@href")

print (link_text_list)
print (link_text_p_list)
print (link_list)
['PyLadies Website', 'PyLadies Meetup', 'PyLadies FB']
['PyLadies Meetup', 'PyLadies FB']
['http://tw.pyladies.com/', 'http://www.meetup.com/PyLadiesTW/', 'https://www.facebook.com/pyladies.tw']

整個網頁中,我們可以看到其實有很多個連結(a標籤),
這種一次存很多資料的變數型別,我們叫做「串列(list)」

串列(list)

  • 可以直接給定有值的串列
  • 也可以給空串列
  • 串列的增加是使用 append()
  • 串列的組成可以不必都是同樣類型的元素
In [244]:
my_list = ["a",2016,5566,"PyLadies"]
my_list2=[]
my_list2.append(2016)
my_list2.append("abc")
print (my_list)
print (my_list2)
['a', 2016, 5566, 'PyLadies']
[2016, 'abc']

串列每個元素的位置

  • Python是從0開始數
  • 可以用負值拿取串列後面的元素
In [9]:
my_list = ["a",2016,5566,"PyLadies",2016,2016.0]
print ("The 1th  element of my_list = ",my_list[0])
print ("The 4th  element of my_list = ",my_list[3])
print ("The last element of my_list = ",my_list[-1]) 
print ("The second-last element of my_list = ",my_list[-2]) 
The 1th  element of my_list =  a
The 4th  element of my_list =  PyLadies
The last element of my_list =  2016.0
The second-last element of my_list =  2016

「字串」 其實是 "每個元素都是一個字母" 的 「串列」

In [239]:
my_string = "PyLadies Taiwan"
print ("The 1st  element of my_string = ",my_string[0])
print ("The 8th  element of my_string = ",my_string[7])
print ("The last element of my_string = ",my_string[-1]) 
The 1st  element of my_string =  P
The 8th  element of my_string =  s
The last element of my_string =  n

字串&串列,可通用的功能

  • len:計算長度
  • count:計算某個元素出現過幾次
In [241]:
my_string = "PyLadies Taiwan"
print ("Length of my_string = ",len(my_string))
print ("The time 'a' appears in my_string = ",my_string.count('a'))
Length of my_string =  15
The time 'a' appears in my_string =  3
In [242]:
my_list = ["a",2016,5566,"PyLadies",2016,2016.0]
print ("Length of my_list = ",len(my_list))
print ("The time '2016' appears in my_string = ",my_list.count(2016))
Length of my_list =  6
The time '2016' appears in my_string =  3

串列功能應用情境

In [16]:
from lxml import etree

html = """
<html>
<head>
  <title>Title</title>
</head>
<body>
  <h1>Subtitle</h1>
  <a href="http://tw.pyladies.com/">PyLadies Website</a>
  <p>
      This is a paragraph <br>
      <a href="http://www.meetup.com/PyLadiesTW/">PyLadies Meetup</a> <br>
      <a href="https://www.facebook.com/pyladies.tw">PyLadies FB</a> <br>
  </p>
  <img src="http://tw.pyladies.com/img/logo2.png" width="99px"/>
</body>
</html>
"""
page = etree.HTML(html)
link_text_list = page.xpath("//a/text()")
print (link_text_list)
print ("網頁中共有",len(link_text_list),"個連結")
['PyLadies Website', 'PyLadies Meetup', 'PyLadies FB']
網頁中共有 3 個連結

Coding Time

  • 使用串列功能,印出網頁中第2個連結網址,也就是"PyLadies Meetup"的網址!

for 迴圈

  • 常用在取出串列的元素
for article_title in page.xpath("//h5/a/text()"):
    print (article_title)

取出 整個文件中 // , 所有 h5 標籤,且小孩是 a 標籤的文字屬性 text() 的結果(串列),
把他們一個個取出,放到變數article_title中,並把它印出來。

in 功能

  • 找出某元素是否在串列中
  • 找出某子字串是否在字串中
In [14]:
my_list = ["a",2016,5566,"PyLadies"]
print ("a" in my_list)
print (5678 in my_list)
True
False
In [15]:
my_string = "PyLadies Taiwan"
print ("a" in my_string)
print ("Py" in my_string)
print ("Python" in my_string)
True
True
False

for 迴圈

In [22]:
my_list = ["a",2016,5566,"PyLadies"]
for element in my_list:
    print (element)
a
2016
5566
PyLadies

Coding Time

  • 使用串列功能,印出網頁中所有連結的文字

PyLadies Website
PyLadies Meetup
PyLadies FB

利用 Chrome 的 「開發人員工具」觀察

  • 對著想要的元素按右鍵>檢查
  • 得知我們想要的資料對應到的原始碼是哪一區段
  • 還可以使用 Copy XPath 快速得知這個元素的 Xpath 語法
    • 對該區段原始碼點右鍵 > Copy > Copy XPath
//*[@id="Blog1"]/div[1]/div[1]/div/div[1]/article/header/h5/a

利用 XPath Helper 來看看會抓到哪些資訊

  • 把看起來雜亂的部分都去掉 //h5/a

Coding Time

進階 - 抓取網頁上的圖片

from lxml import etree
import requests

url = "http://blog.marsw.tw"
response = requests.get(url)
html = response.text

page = etree.HTML(html)

for img_src in page.xpath("//img/@src"):
    # 抓取圖片
    img_response = requests.get(img_src)
    img = img_response.content

    filename=img_src.split("/")[-1]
    filepath="tmp/"+filename
    fileout = open(filepath,"wb")
    fileout.write(img)

注意:圖片的內容不是文字!

  • 所以用的是 content ,而不是像之前抓網頁的時候是用 text ,是取得bytes型式的內容
  • 寫檔的時候,因為取得的是bytes型式的內容,因此用的是 wb ,而不是 w

字串split功能

  • 字串串列 = 原字串.split(子字串):將「原字串」以「子字串」切割為「字串串列」
In [20]:
my_string = "PyLadies Taiwan"
print (my_string.split(" "))
print (my_string.split(" ")[0])
print (my_string.split(" ")[-1])
['PyLadies', 'Taiwan']
PyLadies
Taiwan
In [22]:
img_src = "http://1.bp.blogspot.com/-lP9M5nJ-kb0/U3nAOYRLCAI/AAAAAAAAUaw/1SbrOZBwz3g/s1600/2014-05-01%2B22.26.27.jpg"
print (img_src.split("/"))
print (img_src.split("/")[-1])
['http:', '', '1.bp.blogspot.com', '-lP9M5nJ-kb0', 'U3nAOYRLCAI', 'AAAAAAAAUaw', '1SbrOZBwz3g', 's1600', '2014-05-01%2B22.26.27.jpg']
2014-05-01%2B22.26.27.jpg

More~

  • 瞭解更多的XPath語法 //*[@id="Blog1"]/div[1]/div[1]/div/div[1]/article/header/h5/a
  • 想用某些網頁內的選單查詢功能 -> POST
  • 有些網頁會需要年齡檢測才能進入 -> cookie
  • 取得乾淨的字串、把取得的文字組成文章 -> Python 基礎語法
  • 抓取JavaScript產生的網頁內容
  • 處理需要登入、驗證碼的網頁

請關注 PyLadies 的 「從0到1的網頁爬蟲」系列活動

  • 2nd:更深入的XPath介紹
    • 11/16(三) 19:30~21:30:台北
    • 11/19(六) 09:30~11:30:高雄
    • 11/20(日) 14:30~16:30:台中
  • 3rd:POST、cookie
    • 12/14(三) 19:30~21:30:台北
    • 12/17(六) 09:30~11:30:高雄
    • 12/28(日) 14:30~16:30:台中

Python 基礎語法 學習資源