前言
與其說這是對咨詢公司觀點的總結(jié)绽左,不如說這是一篇爬蟲技術(shù)和文本挖掘技術(shù)的展示。我們試圖抓取數(shù)家咨詢公司發(fā)布關(guān)于中國的報告,并使用文本挖掘技術(shù)分析其觀點凡资。我們不僅將盡可能詳細(xì)地解釋本文使用的代碼渴肉,還將講解寫作代碼的過程冗懦。我們不假設(shè)讀者有python
的經(jīng)驗,但如果有一定的編程經(jīng)驗會很有幫助宾娜。本文不講解python
的安裝過程批狐。
本文是如何運(yùn)行Python程序的
如果您不是程序員,那么您的編程經(jīng)驗或許是這樣:打開一個軟件前塔,比如MATLAB
或者RStudio
嚣艇,在某個用于編輯代碼窗口輸入代碼,然后點擊運(yùn)行按鈕华弓,最后查看某個結(jié)果窗口食零。這樣的軟件通常被稱作IDE (integrated development environment)。Python也有對應(yīng)的IDE寂屏,但我們不建議使用贰谣。相反娜搂,我們建議使用Notepad++
來寫程序。一個簡單的程序如下:
- 在C盤建立一個名為
test.py
的文件 - 用
Notepad++
打開該文件吱抚,插入如下代碼
- 打開
cmd
百宇,移動到當(dāng)前地址,例如此處則輸入cd C:\Work
- 輸入
python test.py
秘豹,即得到如下結(jié)果
獲取研究報告
我們認(rèn)為盡量準(zhǔn)確的携御、不神秘的用語總是有益的。當(dāng)我們說“抓取報告”時既绕,我們希望做的事情大致如下:
- 打開一篇報告所在的網(wǎng)頁
- 打開該網(wǎng)頁的
html
源代碼 - 觀察網(wǎng)頁結(jié)構(gòu)啄刹,找出自己想要的內(nèi)容的位置
- 抓取想要的內(nèi)容
- 打開下一個網(wǎng)頁
沒有什么比例子更好的講解了。讓我們看如下例子凄贩。
獲取麥肯錫公司的報告
在開始前誓军,我們在腳本的最頂端加入如下代碼:
import io
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8')
這三行代碼與抓取文章無關(guān),這是為了保證cmd
能夠顯示某些特定字符疲扎。
下面我們打開一篇報告的網(wǎng)址:http://www.mckinseychina.com/who-is-winning-the-war-for-talent-in-china/
其內(nèi)容如下
我們感興趣的內(nèi)容有文章的標(biāo)題昵时,發(fā)布時間和內(nèi)容本身。現(xiàn)在我們開始講解抓取的過程评肆。
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
這三行代碼是為了加載我們所需要的工具债查。urlopen
的作用是打開一個網(wǎng)頁,而BeautifulSoup
則是一個非常強(qiáng)大的提取html
信息的工具瓜挽。我們也需要re
來處理正則表達(dá)式(后文將介紹)盹廷。我們將在下文看到它們的用法。
值得一提的是久橙,python
有兩種加載的方式俄占。下文可以看出,使用from ... import
時淆衷,我們可以直接使用函數(shù)缸榄,例如html = urlopen(url)
。使用import
我們則需要加上包的名稱祝拯,例如x = re.compile("...")
本文代碼的第一個函數(shù)getMcKArticle(url)
的作用是輸入一篇文章的網(wǎng)址后甚带,提取這篇文章的標(biāo)題、發(fā)布時間佳头、簡介和內(nèi)容鹰贵,并且存儲在一個txt
文檔中。
html = urlopen(url)
soup = BeautifulSoup(html.read())
我們使用urlopen
讀取文章的網(wǎng)址康嘉,將結(jié)果儲存在html
這一變量中碉输。這時html
變量就是網(wǎng)頁的源代碼。之后我們再使用BeautifulSoup
亭珍,為提取信息做準(zhǔn)備敷钾。
讓我們打開網(wǎng)頁的源代碼枝哄,如下圖所示。
我們想要的信息便藏在這混沌中阻荒。對于這混沌的
html
挠锥,我們只需要,既其大部分內(nèi)容都符合如下形式
<tag attribute="value">*some stuff here*</tag>
這里侨赡,tag
是標(biāo)簽的名稱瘪贱,attribute
是標(biāo)簽的屬性,some stuff here
是標(biāo)簽內(nèi)容辆毡。
我們可以嘗試ctrl+f
搜索類似time, date, description, content
等來看看有沒有標(biāo)簽包含了我們想要的信息,經(jīng)過幾次嘗試甜害,我們可以找到如下內(nèi)容:
<meta property="og:title" content="Who is Winning the War for Talent........此處省略" />
<meta property="og:description" content="Despite the trend toward automation, job........此處省略." />
<meta property="article:published_time" content="2016-02-15T13:06:42+00:00" />
<div class="post-content"><p>While much has been made of China’........此處省略.......
以上便是我們想要的內(nèi)容舶掖。要抓取上文的內(nèi)容,我們需要先學(xué)習(xí)一下BeautifulSoup
的一些功能尔店。一個例子勝過千言萬語眨攘。以下例子來自BeautifulSoup
的官方文檔。
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http: //example.com/elsie" class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a> and
<a class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
我們將以上html
存儲在一個叫soup
的BeautifulSoup
對象中嚣州,那么:
soup.title 輸出
<title>The Dormouse's story</title>
soup.findAll('a') 輸出
[<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a>,
<a class="sister" id="link3">Tillie</a>]
soup.find(id="link3") 或者 soup.find({"id": "link3"}) 輸出
<a class="sister" id="link3">Tillie</a>
大致來說鲫售,findAll
輸出所有符合條件的結(jié)果。find
輸出符合條件的第一個結(jié)果该肴。這兩個函數(shù)既可以用來按標(biāo)簽名稱查找情竹,也可以按標(biāo)簽屬性查找。請讀者自行體會匀哄。
于是秦效,我們使用
try:
articleTitle = soup.find("meta",{'property':"og:title"})['content'].strip()
except:
articleTitle = "This article has no title"
來提取文章的標(biāo)題。['content']
的作用是提取出content
這一屬性涎嚼。.strip()
用于將結(jié)果左右的空格刪除阱州。注意我們使用了try: ... except:...
,這是為了防止有的文章不能用這樣的方式找到標(biāo)題導(dǎo)致程序報錯法梯,這樣處理后苔货,如果沒有標(biāo)題,那么我們便指出它沒有標(biāo)題立哑。文章的發(fā)布時間和描述同理夜惭。
對于文章的正文,我們有
try:
TarticlePara = soup.find("div", {"class":"post-content"}).findAll({"p","h1","h2","h3","h4","h5","h6"})
except:
return None
我們觀察到刁憋,文章的正文在<div class="post-content">
這一大標(biāo)簽的<p>, <h2>, <h3>
小標(biāo)簽下滥嘴,為了保險起見,我們將<p>,<h1>,...<h6>
全部都找出來至耻。這樣我們就獲取了文章的正文了若皱。注意镊叁,有的文章不能靠這種方法找到正文,但這樣的文章我們就選擇忽略了走触,于是我們使用return none
來跳出函數(shù)剩下的部分晦譬。
本函數(shù)剩余部分的作用是把結(jié)果輸出成為一個txt
文件。我們用
with open('C:/Work/Scrapers/ConsultingReports/McKinsey/'+articleTitle[0:30]+'.txt','w+', encoding="utf8") as file:
來創(chuàng)建一個標(biāo)題是文章標(biāo)題的前30個字符的txt
文件互广。
file.write('ThisIsTile:'+articleTitle+'\n')
file.write('ThisIsDate:'+articleTime+'\n')
file.write('ThisIsDesc:'+articleDescription+'\n\n')
for para in TarticlePara:
file.write(para.get_text())
我們分別在文件中寫入文章的標(biāo)題敛腌、時間和描述。\n
的作用是換到下一行惫皱。之后我們寫入文章的正文像樊。為什么我們這里要寫一個for
循環(huán)呢?因為和標(biāo)題時間描述不同旅敷,文章的正文是按段落存儲的生棍,TarticlePara
變量是這些段落的集合,所以不能直接寫入到文件里媳谁,我們只能逐個段落地寫入涂滴。
上文函數(shù)的作用是在我們找到一篇文章的網(wǎng)址后,獲取該文章的信息晴音。我們需要第二個函數(shù)柔纵,用以找到這些文章的網(wǎng)址,這便是getArticleLinks(motherUrl)
锤躁。我們將一個起始網(wǎng)址輸入到該函數(shù)中搁料,輸出文章網(wǎng)址的集合。這個函數(shù)的思路如下:
我們先打開一個起始網(wǎng)頁(mother)
系羞,提取出該網(wǎng)頁包含的所有鏈接加缘,選取那些指向文章的鏈接并存儲起來。之后我們跳到一篇文章的網(wǎng)頁(child 1)
觉啊,選取文章鏈接并存儲拣宏。然后跳到下一篇文章(child 2)
,以此類推杠人。
我們需要注意到一個問題勋乾,即我們不可避免的會遇到很多重復(fù)的文章鏈接,怎么保證我們最后輸出的文章網(wǎng)址集合沒有重復(fù)呢嗡善?所幸的是辑莫,python
的set()
可以實現(xiàn)這一功能。當(dāng)我們向一個set
輸入重復(fù)的值時罩引,它只會保留一個記錄各吨。我們將獲取的所有文章鏈接都輸入到一個set
后,最后的結(jié)果就是沒有重復(fù)的文章鏈接集合了袁铐。
我們先使用BeautifulSoup
來存儲起始網(wǎng)址揭蜒,然后調(diào)用一個全局變量articlePages
横浑。我們的目的是將所有文章鏈接都存儲在這個變量里,而且我們需要重復(fù)地使用這個函數(shù)屉更。如果我們選擇在這個函數(shù)的內(nèi)部建立一個新變量并存儲網(wǎng)址的話徙融,那么每次重新調(diào)用這個函數(shù)時,這個變量便會被清空瑰谜,這顯然不是我們想要的欺冀。所以我們在函數(shù)外部創(chuàng)建一個集合articlePages = set()
,然后再在函數(shù)中調(diào)用萨脑。
接下來我們提取所有文章的網(wǎng)址隐轩。觀察源代碼,我們發(fā)現(xiàn)包含網(wǎng)址的標(biāo)簽結(jié)構(gòu)如下:
<a style="display:inline-block;" class="icon link-icon" href="[http://www.mckinseychina.com/how-china-count....此處省略]">
標(biāo)簽名是a
渤早,而我們想要的信息存儲在href
這一屬性中龙助。我們運(yùn)用findAll
函數(shù)來提取所有的網(wǎng)址。注意到這個令人生畏的怪物:
href=re.compile("http://www.mckinseychina.com/([A-Za-z0-9]+-){3,}[A-Za-z0-9]+/")
這里我們使用了正則表達(dá)式蛛芥。我們提出一種模式,然后搜尋能夠匹配該模式的字符串军援。因篇幅所限仅淑,我們沒有辦法對正則表達(dá)式做一個哪怕淺顯的介紹,但我們可以仔細(xì)看看這里這個例子胸哥。
首先我們觀察文章網(wǎng)址都有什么共同點涯竟,請看這三個網(wǎng)址:
http://www.mckinseychina.com/who-is-winning-the-war-for-talent-in-china/
http://www.mckinseychina.com/what-might-happen-in-china-in-2016/
http://www.mckinseychina.com/which-china-headline-do-you-prefer/
我們發(fā)現(xiàn),這些網(wǎng)址的共同點如下:
- 它們都以
http://www.mckinseychina.com
作為開頭 - 它們后接的都是文章的標(biāo)題空厌,每一個單詞或數(shù)字用
-
隔開
現(xiàn)在我們可以開始寫正則表達(dá)式來匹配了這一模式了
-
http://www.mckinseychina.com/
: 到這里庐船,我們規(guī)定網(wǎng)址的開頭 -
([A-Za-z0-9]+-)
:[A-Za-z0-9]
的作用是,匹配任意一個大寫字母嘲更、小寫字母或者數(shù)字筐钟。我們在后邊跟上+
,則表示我們匹配任意次數(shù)赋朦。之后我們跟上-
篓冲,則表示我們匹配鏈接符號-
。我們用(...)
表示這是一個整體宠哄。 -
{3,}
: 這表示我們匹配上一步中的部分3次或以上壹将。為什么要這樣規(guī)定呢?因為觀察網(wǎng)頁毛嫉,我們發(fā)現(xiàn)诽俯,有這樣的網(wǎng)址
http://www.mckinseychina.com/contact-us
以上網(wǎng)頁顯示的是聯(lián)系信息,而這顯然不是我們想要的承粤。而這樣的網(wǎng)址都很短暴区,因此我們匹配3次以上用以忽略這樣的網(wǎng)址闯团。
-
[A-Za-z0-9]+/
: 網(wǎng)址的最后以單詞或數(shù)字加上/
結(jié)束,并且沒有-
颜启,因此我們?nèi)绱瞬僮鳌?br> 很顯然偷俭,以上僅僅是一種匹配的方法,讀者大可以自行觀察其它的匹配方法缰盏。
細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)了涌萤,我們使用了一種很取巧的方法,即我們只在文章之間跳轉(zhuǎn)口猜。我們沒有進(jìn)入類似于
http://www.mckinseychina.com/insights
http://www.mckinseychina.com/insights/innovation/
這類的網(wǎng)頁负溪。讀者在掌握了本文的技巧后,可以自行修改代碼以讓我們的文章爬取更完善济炎。
值得一提的是川抡,正則表達(dá)式是一個很反人類的工具,很容易出錯须尚,一個很好的寫正則表達(dá)式的輔助工具是http://regexpal.isbadguy.com/
崖堤。
接下來,我們開始抓取和存儲這些鏈接了耐床。
for link in links:
newArticlePage = link.attrs['href']
articlePages.add(newArticlePage)
print(str(len(articlePages)) + " preys slayed")
我們提取出這些網(wǎng)址密幔,用.add()
方法來把這些網(wǎng)址加入到articlePages
這個集合里。我們用print(str(len(articlePages)) + " preys slayed")
來顯示集合里已經(jīng)存儲了多少篇文章撩轰,用以觀察進(jìn)度胯甩。
if len(articlePages)>=20:
print("Hunting complete")
else:
getArticleLinks(newArticlePage)
return articlePages
如果我們找到了20個以上的文章就滿足了,如果還沒有達(dá)到這個數(shù)字堪嫂,我們就選擇該網(wǎng)頁中最后一篇文章作為新的起始網(wǎng)頁偎箫,重復(fù)以上行為。最后我們輸出文章集合皆串。
有了上述兩個函數(shù)淹办,我們可以真正開始抓取文章了。
motherUrl = "http://www.mckinseychina.com"
articlePages = set()
articlePages = getArticleLinks(motherUrl)
summonCounter = 1
for page in list(articlePages):
getMcKArticle(page)
print(str(summonCounter) + ' out of ' + str(len(articlePages)) + " nightmares slayed")
summonCounter += 1
print("Farewell good hunter. May you find worth in the waking world")
- 選擇起始網(wǎng)頁
- 創(chuàng)建文章集合
- 用
getArticleLinks()
填充文章集合 - 對文章集合中的每個網(wǎng)址恶复,用
getMcKArticle()
下載其內(nèi)容
注意我們用了summonCounter
來監(jiān)督抓取進(jìn)度娇唯。
這樣我們就完成了文章的下載。print
一句您喜歡的話來表揚(yáng)下自己吧寂玲!
附錄
獲取麥肯錫公司報告的完整代碼
import io
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8')
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
def getMcKArticle(url):
html = urlopen(url)
soup = BeautifulSoup(html.read())
try:
articleTitle = soup.find("meta",{'property':"og:title"})['content'].strip()
except:
articleTitle = "This article has no title"
try:
articleTime = soup.find("meta", {"property":"article:published_time"})['content'].strip()
except:
articleTime = "This article has no date"
try:
articleDescription = soup.find("meta",{'property':"og:description"})['content'].strip()
except:
articleDescription = "This article has no description"
try:
TarticlePara = soup.find("div", {"class":"post-content"}).findAll({"p","h1","h2","h3","h4","h5","h6"})
except:
return None
with open('C:/Work/Scrapers/ConsultingReports/McKinsey/'+articleTitle[0:30]+'.txt','w+', encoding="utf8") as file:
file.write('ThisIsTile:'+articleTitle+'\n')
file.write('ThisIsDate:'+articleTime+'\n')
file.write('ThisIsDesc:'+articleDescription+'\n\n')
for para in TarticlePara:
file.write(para.get_text())
def getArticleLinks(motherUrl):
html = urlopen(motherUrl)
soup = BeautifulSoup(html)
global articlePages
links = soup.findAll("a", href=re.compile("http://www.mckinseychina.com/*([A-Za-z0-9]+-){3,}[A-Za-z0-9]+/"))
for link in links:
newArticlePage = link.attrs['href']
articlePages.add(newArticlePage)
print(str(len(articlePages)) + " preys slayed")
if len(articlePages)>=20:
print("Hunting complete")
else:
getArticleLinks(newArticlePage)
return articlePages
motherUrl = "http://www.mckinseychina.com"
articlePages = set()
articlePages = getArticleLinks(motherUrl)
summonCounter = 1
for page in list(articlePages):
getMcKArticle(page)
print(str(summonCounter) + ' out of ' + str(len(articlePages)) + " nightmares slayed")
summonCounter += 1
print("Farewell good hunter. May you find worth in the waking world")