Python 網路爬蟲入門

Speaker : Mars

2017/10/06

Roadmap

  • Python 簡介
  • 第一隻網路爬蟲
  • 物件與字串
  • 檔案輸出
  • 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
  • [各種應用]

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 環境安裝

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

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

第一隻網路爬蟲

網頁的運作原理

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

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

http://blog.marsw.tw 為例

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

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

  • 取得文章類別
  • 取得文章標題
  • ......etc.
In [1]:
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 日本 ~ 高瀬川、鴨川、祇園之美

抓取傳遞資料的方法為 GET 的網頁

from lxml import etree

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

import requests

裝備 requests 工具箱

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

我們把 "http://blog.marsw.tw" 這個<字串 string>物件的儲存空間
以名稱 url 指向這著儲存空間。

response = requests.get(url)

使用 requests 工具箱的 get 工具,
這個工具能幫我們抓下網址為 url 的網頁(傳遞資料的方法為 GET )上的資料,
以我們命名的response名稱指向這些資料的儲存空間。

html = response.text

response 屬於 text 的資料,也就是網頁原始碼(response是用get抓下來的網頁資料)
以我們命名的html名稱指向。

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

我們先來看看 物件命名 以及 <字串 string>

物件

  • 在Python中,所有東西皆是物件
  • 在Python中,「=」可以想像成是「貼標籤」的意思
    • 在右邊的「物件」(資料內容)存放位置貼上左邊「名稱」的標籤,亦即名稱指向物件
  • 在Python中,沒有命名的物件會被回收,有了命名物件可以讓資料重複利用
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)

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

print (my_string)
print (my_string2)
PyLadies Taiwan
PyLadies

字串格式化方法 format 應用情境

In [17]:
# 產生各股票網址(股票代號是在網址中,不是在尾端)
url = "http://www.wantgoo.com/stock/{}?searchType=stocks".format(2330)
print (url)
url = "http://www.wantgoo.com/stock/{}?searchType=stocks".format(2371)
print (url)
http://www.wantgoo.com/stock/2330?searchType=stocks
http://www.wantgoo.com/stock/2371?searchType=stocks
In [4]:
# 產生facebook粉絲頁資訊(想讓資訊好看,不想用+來處理)
url = "https://www.facebook.com/{}/{}/".format("pyladies.tw","about")
print (url)
url = "https://www.facebook.com/{}/{}/".format("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 [5]:
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 [6]:
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()

Windows 版本、網頁亂碼、改變檔案路徑

from lxml import etree
import requests

url = "http://blog.marsw.tw"
response = requests.get(url)
response.encoding = 'utf-8'
html = response.text

fileout = open(r"C:\Users\Desktop\test.html","w",encoding="utf8")
fileout.write(html)
fileout.close()

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

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

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

You can try

  • 物件名稱可以自行更改
In [7]:
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>

Nodes

  • 小孩/:「下一層節點」,或是該標籤的「屬性 @」或「文字 text()
  • 子孫//:小孩、小孩的小孩、小孩的小孩的小孩......etc.
  • . 以現在的節點node搜索,常用在同時呈現同一階層(輩份)的資料

範例

  • body
    • 小孩:h1、a(Website)、p、img
    • 子孫:h1、text()、a、@href、p、img、@src、@width
  • a
    • 小孩:@href、text()
<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>
In [8]:
# 這邊先不使用request去抓,直接將原始碼貼上 html 標間,方便大家理解
# 遇到長篇文章有換行的存在,可用「三個」雙引號或單引號
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()")
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)

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

串列索引與長度

  • Python是從0開始數
  • 可以用負值拿取串列後面的元素
  • len(串列):可取得串列長度
In [10]:
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]) 
print ("Length of my_list = ",len(my_list))
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
Length of my_list =  6
first_article_tags = page.xpath("//footer/a/text()")[0]

前面的程式碼,我們已經讓page指向「XPath的節點(node)型態」的物件上,

這邊的程式碼是以page指向的資料用xpath找尋
整個文件中 // , 所有 footer 標籤,且小孩是 a 標籤的文字屬性 text()
而我們只取「第1個」結果,以名稱 first_article_tags 指向他。

print (first_article_tags)

first_article_tags指向的資料,印到螢幕上。

串列功能應用情境

In [11]:
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"的網址!

Hint:

  • page.xpath("//a/text()") 是找出所有連結的小孩是文字的結果,
    文字是連結標籤a的小孩,連結網址也是連結標籤a的小孩
  • 屬性是用 「@屬性名稱
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_list = page.xpath("______")
print (____)

for 迴圈

  • 常用在取出串列的元素
In [12]:
my_list = ["a",2016,5566,"PyLadies"]
for element in my_list:
    print (element)
a
2016
5566
PyLadies
for article_title in page.xpath("//h5/a/text()"):
    print (article_title)

取出 整個文件中 // , 所有 h5 標籤,且小孩是 a 標籤的文字屬性 text() 的結果(串列),
把它們一個個取出,以名稱article_title指向它,並把它印出來。

Coding Time

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

PyLadies Website
PyLadies Meetup
PyLadies FB

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 = _____
___ text ___ page.xpath("_____"):
    print(text)  # 記得要縮排

利用 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

進階 - 同層級語法

In [13]:
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)
for link_node in page.xpath("//a"):
    text = link_node.xpath("./text()")[0]
    link = link_node.xpath("./@href")[0]
    print (text,link)
PyLadies Website http://tw.pyladies.com/
PyLadies Meetup http://www.meetup.com/PyLadiesTW/
PyLadies FB https://www.facebook.com/pyladies.tw

進階 - 抓取網頁上的圖片

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)

注意:

  • 資料夾名稱為tmp,記得要新建資料夾,不然會找不到路徑
  • 圖片的內容不是文字!
    • 所以用的是 content(bytes型式的內容)
    • 寫檔的時候,因為取得的是bytes型式的內容,因此用的是 wb

字串split功能

  • 字串串列 = 原字串.split(子字串):將「原字串」以「子字串」切割為「字串串列」
In [14]:
my_string = "PyLadies Taiwan"
print (my_string.split(" "))
print (my_string.split(" ")[0])
print (my_string.split(" ")[-1])
['PyLadies', 'Taiwan']
PyLadies
Taiwan
In [15]:
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

總複習與實戰技巧

In [16]:
from lxml import etree
import requests

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

# Save to File
fileout = open("test.html","w") 
fileout.write(html)
fileout.close()

# Parsing
page = etree.HTML(html)
for article_title_node in page.xpath("//h5/a"):
    text = article_title_node.xpath("./text()")[0]
    link = article_title_node.xpath("./@href")[0]
    print (text,link)
2014。09~關西9天滾出趣體驗 http://blog.marsw.tw/2014/09/2014099.html
2014。05 日本 ~ Day9 旅程的最後~滾出趣精神、令人屏息的司馬遼太郎紀念館 http://blog.marsw.tw/2014/09/201405-day9.html
2014。05 日本 ~ 滾出趣(調查11) 道頓堀的街頭運動 http://blog.marsw.tw/2014/09/201405-11.html
2014。05 日本 ~ Day8 鞍馬天氣晴、貴船川床流水涼麵、京都風情 http://blog.marsw.tw/2014/09/201405-day8.html
2014。05 日本 ~ 高瀬川、鴨川、祇園之美 http://blog.marsw.tw/2014/09/201405.html

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

  • Network 觀察各個request => requests
  • Elements 輔助觀察經Goolge排版過的原始碼架構 => lxml
    • 但要看原始碼才是最準的!(右鍵>檢視原始碼)
    • 可利用Copy Xpath幫忙解析

觀察細節!try & error!

共同之處?

  • //*[@id="Blog1"]/div[1]/div[1]/div/div[1]/article/header/h5/a
  • //*[@id="Blog1"]/div[1]/div[1]/div/div[2]/article/header/h5/a
    • 可以只用//article/header/h5/a,甚至更簡短的//h5/a就可以達到同樣效果!

找出特別之處

pchome的網址無法直接拿到商品資訊

More~

PyLadies 系列活動