實戰項目 1:5 行代碼爬取國內所有上市公司信息
Python入門爬蟲與數據分析
在正式開始這門專欄課的學習之前,我們先來看一個簡單的爬蟲案例。興趣是最好的老師,當你對爬蟲產生興趣的時候,才會更有動力去學它。
▌入門爬蟲
首先來看要爬取的目標網站:http://s.askci.com/stock/1/
網頁中有一張表格,內容是全國上市公司相關信息,整個表格有 180 頁。我們需要做的工作就是,用幾十秒鍾把表格所有數據爬取下來,接着保存到本地文件。試想如果不會爬蟲,要完成這份工作得費多大力氣。
為什么要以這個網頁作為第一個爬蟲案例呢?有兩點原因:
- 這類表格型數據在網頁中非常常見,學會這個爬蟲就能爬取一大類的網頁數據,很實用。
- 這個爬蟲很簡單,5 行代碼就可以實現。
好,下面我們就正式開始。
▌簡版代碼
我們可以先寫一個簡版代碼,只寫最核心的,就是抓數據,其他的諸如:下載速度、存儲方式、代碼條理性等先不管,這樣代碼寫起來容易上手,能增強信心。
下面來看看如何用 5 行代碼抓取上面表格中的所有數據。
-
1 import pandas as pd 2 import csv 3 for i in range(1,178): # 爬取全部頁 4 tb = pd.read_html('http://s.askci.com/stock/a/?reportTime=2017-12-31&pageNum=%s' % (str(i)))[3] 5 tb.to_csv('company.csv', mode='a', encoding='utf_8_sig', header=1, index=0)
如果你不太明白上面代碼意思,沒有關系,后面的課程會介紹。現在只需要動手敲一遍,然后點擊運行,幾十秒鍾之后在本地就可以看到一個名為 company.csv
的文件,打開結果見下表:
這樣我們就爬取完了所有數據。怎么樣,是不是覺得爬蟲有點意思,沒有想象中那么難。寫幾行代碼,剩下的交給電腦就好了。
上面的爬蟲有些單薄,還可以更完善一些,具體考慮這幾個方面:
▌完善代碼
- 增加代碼靈活性
上面代碼中的 URL 參數是固定的,比如reportTime=2017-12-31
表示爬取的是這一日期的數據,如果想爬取其他時期,需要在 URL 中去修改,不夠靈活方便。怎么改變呢,也很簡單,可以將日期賦予一個變量,在 URL 外部單獨修改變量來爬取不同日期的數據。
- 增添存儲方式
上面文件保存方式選擇了 csv 文件,更為常見的方式是保存到數據庫中,比如 MySQL、MongoDB 等,這里我們可以選擇保存到 MySQL 中,當練習數據庫的使用。
- 加快爬取速度
上面的代碼是單進程爬取,爬取 180 頁速度相對較慢,要想加快爬取速度可以使用多進程方式。
- 增加異常處理
上面代碼沒有任何異常處理措施,一旦爬取失敗,我們找不到原因。最好是增加代碼異常捕捉方式,可以使用 try except 、if 等語句,讓代碼更健壯。
考慮上述幾方面,代碼完善如下:
-
1 import requests 2 import pandas as pd 3 from bs4 import BeautifulSoup 4 from lxml import etree 5 import time 6 import pymysql 7 from sqlalchemy import create_engine 8 from urllib.parse import urlencode # 編碼 URL 字符串 9 start_time = time.time() #計算程序運行時間 10 def get_one_page(i,date): 11 try: 12 headers = { 13 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' 14 } 15 paras = { 16 'reportTime': date, 17 #可以改報告日期,比如 2018-6-30 獲得的就是該季度的信息 18 'pageNum': i #頁碼 19 } 20 url = 'http://s.askci.com/stock/a/?' + urlencode(paras) 21 response = requests.get(url,headers = headers) 22 if response.status_code == 200: 23 return response.text 24 return None 25 except RequestException: 26 print('爬取失敗') 27 def parse_one_page(html): 28 soup = BeautifulSoup(html,'lxml') 29 content = soup.select('#myTable04')[0] #[0]將返回的 list 改為 bs4 類型 30 tbl = pd.read_html(content.prettify(),header = 0)[0] 31 # prettify()優化代碼,[0]從 pd.read_html 返回的 list 中提取出 DataFrame 32 tbl.rename(columns = {'序號':'serial_number', '股票代碼':'stock_code', '股票簡稱':'stock_abbre', '公司名稱':'company_name', '省份':'province', '城市':'city', '主營業務收入(201712)':'main_bussiness_income', '凈利潤(201712)':'net_profit', '員工人數':'employees', '上市日期':'listing_date', '招股書':'zhaogushu', '公司財報':'financial_report', '行業分類':'industry_classification', '產品類型':'industry_type', '主營業務':'main_business'},inplace = True) 33 return tbl 34 def generate_mysql(): 35 conn = pymysql.connect( 36 host='localhost', 37 user='root', 38 password='******', #修改為你的密碼 39 port=3306, 40 charset = 'utf8', 41 db = 'wade') #修改為自己的數據庫 42 cursor = conn.cursor() 43 sql = 'CREATE TABLE IF NOT EXISTS listed_company (serial_number INT(20) NOT NULL,stock_code INT(20) ,stock_abbre VARCHAR(20) ,company_name VARCHAR(20) ,province VARCHAR(20) ,city VARCHAR(20) ,main_bussiness_income VARCHAR(20) ,net_profit VARCHAR(20) ,employees INT(20) ,listing_date DATETIME(0) ,zhaogushu VARCHAR(20) ,financial_report VARCHAR(20) , industry_classification VARCHAR(20) ,industry_type VARCHAR(100) ,main_business VARCHAR(200) ,PRIMARY KEY (serial_number))' 44 cursor.execute(sql) 45 conn.close() 46 def write_to_sql(tbl, db = 'wade'): 47 engine = create_engine('mysql+pymysql://root:******@localhost:3306/{0}?charset=utf8'.format(db)) 48 try: 49 tbl.to_sql('listed_company2',con = engine,if_exists='append',index=False) 50 # append 表示在原有表基礎上增加,但該表要有表頭 51 except Exception as e: 52 print(e) 53 def main(page): 54 generate_mysql() 55 date = '2017-12-31' 56 for i in range(1,page): 57 html = get_one_page(i,date) 58 tbl = parse_one_page(html) 59 write_to_sql(tbl) 60 # # 單進程 61 # if __name__ == '__main__': 62 # main(178) 63 # endtime = time.time()-start_time 64 # print('程序運行了%.2f 秒' %endtime) 65 # # 多進程 66 from multiprocessing import Pool 67 if __name__ == '__main__': 68 pool = Pool(4) 69 pool.map(main, [i for i in range(1,178)]) #共有 178 頁 70 endtime = time.time()-start_time 71 print('程序運行了%.2f 秒' %(time.time()-start_time))
代碼從原先的 5 行增加到幾十行,針對每個點去完善,代碼編寫過程也很自然,如果一上來就寫出這幾十行代碼,新手可能很快就會放棄。
數據爬取下來之后,可以說爬蟲工作就完成了,不過,還可以進一步做一些數據分析,比如像下面這樣:
以上,我們從一個簡單的爬蟲案例入手,初步了解了爬蟲是怎么回事,能干什么事。代碼具體編寫知識,后續課程一一介紹。
文中完整代碼和素材,可以在下方鏈接中得到:
另外,如果想更充分地學習本專欄課程,可以參考《Python3 網絡爬蟲開發實戰》這本書。
下一節課,我們再用一個實戰來學習爬蟲基本技法。