10行程式碼爬取全國所有A股/港股/新三板上市公司資訊
點選上方“ 程式人生 ”,選擇“置頂公眾號”
第一時間關注程式猿(媛)身邊的故事
作者
高階農民工
已獲原作者授權,如需轉載,請聯絡原作者。
摘要:我們平常在瀏覽網頁中會遇到一些表格型的資料資訊,除了表格本身體現的內容以外,可能還想透過表格背後再挖掘些有意思或者有價值的資訊。這時,可用python爬蟲來實現。本文采用pandas庫中的read_html方法來快速準確地抓取網頁中的表格資料。
由於本文中含有一些超連結,微信中無法直接開啟,所以建議複製連結到瀏覽器開啟:
https://www.makcyun.top/web_scraping_withpython2.html
本文知識點:
1. table型表格
我們在網頁上會經常看到這樣一些表格,比如:
QS2018世界大學排名:

財富世界500強企業排名:

IMDB世界電影票房排行榜:

中國A股上市公司資訊:

它們除了都是表格以外,還一個共同點就是當點選右鍵-定位時,可以看到它們都是table型別的表格。




從中可以看到table型別的表格網頁結構大致如下:
1<table class="..." id="..."> 2<thead> 3<tr> 4<th>...</th> 5</tr> 6</thead> 7<tbody> 8<tr> 9<td>...</td> 10</tr> 11<tr>...</tr> 12<tr>...</tr> 13<tr>...</tr> 14<tr>...</tr> 15... 16<tr>...</tr> 17<tr>...</tr> 18<tr>...</tr> 19<tr>...</tr> 20</tbody> 21</table>
先來簡單解釋一下上文出現的幾種標籤含義:
1<table>: 定義表格 2<thead>: 定義表格的頁首 3<tbody>: 定義表格的主體 4<tr>: 定義表格的行 5<th>: 定義表格的表頭 6<td>: 定義表格單元
這樣的表格資料,就可以利用pandas模組裡的read_html函式方便快捷地抓取下來。下面我們就來操作一下。
2. 快速抓取
下面以中國上市公司資訊這個網頁中的表格為例,感受一下read_html函式的強大之處。
1import pandas as pd 2import csv 3 4for i in range(1,178):# 爬取全部177頁資料 5url = 'http://s.askci.com/stock/a/?reportTime=2017-12-31&pageNum=%s' % (str(i)) 6tb = pd.read_html(url)[3] #經觀察發現所需表格是網頁中第4個表格,故為[3] 7tb.to_csv(r'1.csv', mode='a', encoding='utf_8_sig', header=1, index=0) 8print('第'+str(i)+'頁抓取完成')

只需不到十行程式碼,1分鐘左右就可以將全部178頁共3535家A股上市公司的資訊乾淨整齊地抓取下來。比採用正則表示式、xpath這類常規方法要省心省力地多。如果採取人工一頁頁地複製貼上到excel中,就得操作到猴年馬月去了。
上述程式碼除了能爬上市公司表格以外,其他幾個網頁的表格都可以爬,只需做簡單的修改即可。因此,可作為一個簡單通用的程式碼模板。但是,為了讓程式碼更健壯更通用一些,接下來,以爬取177頁的A股上市公司資訊為目標,講解一下詳細的程式碼實現步驟。
3. 詳細程式碼實現
3.1. read_html函式
先來了解一下 read_html 函式的api:
1pandas.read_html(io, match='.+', flavor=None, header=None, index_col=None, skiprows=None, attrs=None, parse_dates=False, tupleize_cols=None, thousands=', ', encoding=None, decimal='.', converters=None, na_values=None, keep_default_na=True, displayed_only=True) 2 3常用的引數: 4io:可以是url、html文字、本地檔案等; 5flavor:解析器; 6header:標題行; 7skiprows:跳過的行; 8attrs:屬性,比如 attrs = {'id': 'table'}; 9parse_dates:解析日期 10 11注意:返回的結果是**DataFrame**組成的**list**。
參考:
1 http://pandas.pydata.org/pandas-docs/stable/io.html#io-read-html 2 http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_html.html
3.2. 分析網頁url
首先,觀察一下中商情報網第1頁和第2頁的網址:
1http://s.askci.com/stock/a/?reportTime=2017-12-31&pageNum=1#QueryCondition 2http://s.askci.com/stock/a/?reportTime=2017-12-31&pageNum=2#QueryCondition
可以發現,只有 pageNum 的值隨著翻頁而變化,所以基本可以斷定pageNum=1代表第1頁,pageNum=10代表第10頁,以此類推。這樣比較容易用for迴圈構造爬取的網址。
試著把 #QueryCondition 刪除,看網頁是否同樣能夠開啟,經嘗試發現網頁依然能正常開啟,因此在構造url時,可以使用這樣的格式:
http://s.askci.com/stock/a/?reportTime=2017-12-31&pageNum=i
再注意一下其他引數:
a :表示A股,把a替換為 h ,表示 港股 ;把a替換為 xsb ,則表示 新三板 。那麼,在網址分頁for迴圈外部再加一個for迴圈,就可以爬取這三個股市的股票了。
3.3. 定義函式
將整個爬取分為網頁提取、內容解析、資料儲存等步驟,依次建立相應的函式。
1# 網頁提取函式 2def get_one_page(i): 3try: 4headers = { 5'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' 6} 7paras = { 8'reportTime': '2017-12-31', 9#可以改報告日期,比如2018-6-30獲得的就是該季度的資訊 10'pageNum': i#頁碼 11} 12url = 'http://s.askci.com/stock/a/?' + urlencode(paras) 13response = requests.get(url,headers = headers) 14if response.status_code == 200: 15return response.text 16return None 17except RequestException: 18print('爬取失敗') 19 20# beatutiful soup解析然後提取表格 21def parse_one_page(html): 22soup = BeautifulSoup(html,'lxml') 23content = soup.select('#myTable04')[0] #[0]將返回的list改為bs4型別 24tbl = pd.read_html(content.prettify(),header = 0)[0] 25# prettify()優化程式碼,[0]從pd.read_html返回的list中提取出DataFrame 26 27tbl.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) 28 29print(tbl) 30# return tbl 31# rename將表格15列的中文名改為英文名,便於儲存到mysql及後期進行資料分析 32# tbl = pd.DataFrame(tbl,dtype = 'object') #dtype可統一修改列格式為文字 33 34# 主函式 35def main(page): 36for i in range(1,page):# page表示提取頁數 37html = get_one_page(i) 38parse_one_page(html) 39 40# 單程序 41if __name__ == '__main__': 42main(178)#共提取n頁
上面兩個函式相比於快速抓取的方法程式碼要多一些,如果需要抓的表格很少或只需要抓一次,那麼推薦快速抓取法。如果頁數比較多,這種方法就更保險一些。解析函式用了BeautifulSoup和css選擇器,這種方法定位提取表格所在的 id為#myTable04 的table程式碼段,更為準確。
3.4. 儲存到MySQL
接下來,我們可以將結果儲存到本地csv檔案,也可以儲存到MySQL資料庫中。這裡為了練習一下MySQL,因此選擇儲存到MySQL中。
首先,需要先在資料庫建立存放資料的表格,這裡命名為 listed_company 。程式碼如下:
1import pymysql 2 3def generate_mysql(): 4conn = pymysql.connect( 5host='localhost',# 本地伺服器 6user='root', 7password='******',# 你的資料庫密碼 8port=3306,# 預設埠 9charset = 'utf8', 10db = 'wade') 11cursor = conn.cursor() 12 13sql = 'CREATE TABLE IF NOT EXISTS listed_company2 (serial_number INT(30) NOT NULL,stock_code INT(30) ,stock_abbre VARCHAR(30) ,company_name VARCHAR(30) ,province VARCHAR(30) ,city VARCHAR(30) ,main_bussiness_income VARCHAR(30) ,net_profit VARCHAR(30) ,employees INT(30) ,listing_date DATETIME(0) ,zhaogushu VARCHAR(30) ,financial_report VARCHAR(30) , industry_classification VARCHAR(255) ,industry_type VARCHAR(255) ,main_business VARCHAR(255) ,PRIMARY KEY (serial_number))' 14# listed_company是要在wade資料庫中建立的表,用於存放資料 15 16cursor.execute(sql) 17conn.close() 18 19generate_mysql()
上述程式碼定義了generate_mysql()函式,用於在MySQL中wade資料庫下生成一個listed_company的表。表格包含15個列欄位。根據每列欄位的屬性,分別設定為INT整形(長度為30)、VARCHAR字元型(長度為30) 、DATETIME(0) 日期型等。
在Navicat中檢視建立好之後的表格:


接下來就可以往這個表中寫入資料,程式碼如下:
1import pymysql 2from sqlalchemy import create_engine 3 4def write_to_sql(tbl, db = 'wade'): 5engine = create_engine('mysql+pymysql://root:******@localhost:3306/{0}?charset=utf8'.format(db)) 6# db = 'wade'表示儲存到wade這個資料庫中,root後面的*是密碼 7try: 8tbl.to_sql('listed_company',con = engine,if_exists='append',index=False) 9# 因為要迴圈網頁不斷資料庫寫入內容,所以if_exists選擇append,同時該表要有表頭,parse_one_page()方法中df.rename已設定 10except Exception as e: 11print(e)
以上就完成了單個頁面的表格爬取和儲存工作,接下來只要在main()函式進行for迴圈,就可以完成所有總共178頁表格的爬取和儲存,完整程式碼如下:
1import requests 2import pandas as pd 3from bs4 import BeautifulSoup 4from lxml import etree 5import time 6import pymysql 7from sqlalchemy import create_engine 8from urllib.parse import urlencode# 編碼 URL 字串 9 10start_time = time.time()#計算程式執行時間 11 12def get_one_page(i): 13try: 14headers = { 15'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' 16} 17paras = { 18'reportTime': '2017-12-31', 19#可以改報告日期,比如2018-6-30獲得的就是該季度的資訊 20'pageNum': i#頁碼 21} 22url = 'http://s.askci.com/stock/a/?' + urlencode(paras) 23response = requests.get(url,headers = headers) 24if response.status_code == 200: 25return response.text 26return None 27except RequestException: 28print('爬取失敗') 29 30 31def parse_one_page(html): 32soup = BeautifulSoup(html,'lxml') 33content = soup.select('#myTable04')[0] #[0]將返回的list改為bs4型別 34tbl = pd.read_html(content.prettify(),header = 0)[0] 35# prettify()優化程式碼,[0]從pd.read_html返回的list中提取出DataFrame 36tbl.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) 37 38# print(tbl) 39return tbl 40# rename將中文名改為英文名,便於儲存到mysql及後期進行資料分析 41# tbl = pd.DataFrame(tbl,dtype = 'object') #dtype可統一修改列格式為文字 42 43def generate_mysql(): 44conn = pymysql.connect( 45host='localhost', 46user='root', 47password='******', 48port=3306, 49charset = 'utf8', 50db = 'wade') 51cursor = conn.cursor() 52 53sql = '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))' 54# listed_company是要在wade資料庫中建立的表,用於存放資料 55 56cursor.execute(sql) 57conn.close() 58 59 60def write_to_sql(tbl, db = 'wade'): 61engine = create_engine('mysql+pymysql://root:******@localhost:3306/{0}?charset=utf8'.format(db)) 62try: 63# df = pd.read_csv(df) 64tbl.to_sql('listed_company2',con = engine,if_exists='append',index=False) 65# append表示在原有表基礎上增加,但該表要有表頭 66except Exception as e: 67print(e) 68 69 70def main(page): 71generate_mysql() 72for i in range(1,page): 73html = get_one_page(i) 74tbl = parse_one_page(html) 75write_to_sql(tbl) 76 77# # 單程序 78if __name__ == '__main__': 79main(178) 80 81endtime = time.time()-start_time 82print('程式運行了%.2f秒' %endtime) 83 84 85# 多程序 86# from multiprocessing import Pool 87# if __name__ == '__main__': 88#pool = Pool(4) 89#pool.map(main, [i for i in range(1,178)])#共有178頁 90 91#endtime = time.time()-start_time 92#print('程式運行了%.2f秒' %(time.time()-start_time))
最終,A股所有3535家企業的資訊已經爬取到mysql中,如下圖:

除了A股,還可以順便再把港股和新三板所有的上市公司也爬了。後期,將會對爬取的資料做一下簡單的資料分析。
最後,需說明不是所有表格都可以用這種方法爬取,比如這個網站中的表格,表面是看起來是表格,但在html中不是前面的table格式,而是list列表格式。這種表格則不適用read_html爬取。得用其他的方法,比如selenium,以後再進行介紹。

本文完。
- The End -
點選文末 閱讀全文 ,看『程式人生』其他精彩文章推薦
「若你有原創文章想與大家分享,歡迎投稿。」
加編輯微信ID,備註#投稿#:
程式 丨 druidlost
小七 丨 duoshangshuang
推薦閱讀:
-
ofollow,noindex" target="_blank"> 佛系張小龍和他的微信帝國 | 暢言
-
網際網路公司時尚穿搭指南
-
OA==&mid=2247498597&idx=1&sn=32e9598ad584aab9eeb3150599f2bb78&scene=21#wechat_redirect" rel="nofollow,noindex" target="_blank">來呀!AI喊你鬥地主——首個搞定鬥地主的深度神經網路
-
程式設計師的江湖 務必掌握這些黑話!
print_r('點個贊吧'); var_dump('點個贊吧'); NSLog(@"點個贊吧!") System.out.println("點個贊吧!"); console.log("點個贊吧!"); print("點個贊吧!"); printf("點個贊吧!\n"); cout << "點個贊吧!" << endl; Console.WriteLine("點個贊吧!"); fmt.Println("點個贊吧!") Response.Write("點個贊吧"); alert(’點個贊吧’)