使用Selenium爬取網站表格類數據


本文轉載自一下網站:Python爬蟲(5):Selenium 爬取東方財富網股票財務報表 https://www.makcyun.top/web_scraping_withpython5.html

 

需要學習的地方:

1.Selenium的安裝,配置

2.Selenium的初步使用(自動翻頁)

 

利用Selenium爬取東方財富網各上市公司歷年的財務報表數據。

摘要: 現在很多網頁都采取JavaScript進行動態渲染,其中包括Ajax技術。上一篇文章通過分析Ajax接口數據,順利爬取了澎湃新聞網動態網頁中的圖片。但有的網頁雖然也Ajax技術,但接口參數可能是加密的無法直接獲得,比如淘寶;有的動態網頁也采用JavaScript,但不是Ajax技術,比如Echarts官網。所以,當遇到這兩類網頁時,上一篇文章中的方法便不再奏效,需要新的采取新的方法,這其中包括干脆、直接、好用的的Selenium大法。東方財富網的財務報表網頁也是通過JavaScript動態加載的,本文利用Selenium方法爬取該網站上市公司的財務報表數據。

[TOC]

1. 實戰背景

很多網站都提供上市公司的公告、財務報表等金融投資信息和數據,比如:騰訊財經、網易財經、新浪財經、東方財富網等。這之中,發現東方財富網的數據非常齊全。

東方財富網有一個數據中心:http://data.eastmoney.com/center/,該數據中心提供包括特色數據、研究報告、年報季報等在內的大量數據(見下圖)。

以年報季報類別為例,我們點開該分類查看一下2018年中報(見下圖),可以看到該分類下又包括:業績報表、業績快報、利潤表等7個報表的數據。以業績報表為例,報表包含全部3000多只股票的業績報表數據,一共有70多頁。

假如,我們想獲取所有股票2018年中的業績報表數據,然后對該數據進行一些分析。采取手動復制的方法,70多頁可以勉強完成。但如果想獲取任意一年、任意季度、任意報表的數據,要再通過手動復制的方法,工作量會非常地大。舉個例子,假設要獲取10年間(40個季度)、所有7個報表的數據,那么手動復制的工作量大約將是:40×7×70(每個報表大約70頁),差不多要重復性地復制2萬次!!!可以說是人工不可能完成的任務。所以,本文的目標就是利用Selenium自動化技術,爬取年報季報類別下,任意一年(網站有數據至今)、任意財務報表數據。我們所需要做的,僅是簡單輸入幾個字符,其他就全部交給電腦,然后過一會兒打開excel,就可以看到所需數據”靜靜地躺在那里”,是不是挺酷的?

好,下面我們就開始實操一下。首先,需要分析要爬取的網頁對象。

2. 網頁分析

之前,我們已經爬過表格型的數據,所以對表格數據的結構應該不會太陌生,如果忘了,可以再看一下這篇文章:https://www.makcyun.top/web_scraping_withpython2.html

我們這里以上面的2018年中報的業績報表為例,查看一下表格的形式。

網址url:http://data.eastmoney.com/bbsj/201806/lrb.htmlbbsj代表年報季報,201803代表2018年一季報,類似地,201806表示年中報;lrb利潤表的首字母縮寫,同理,yjbb表示業績報表。可以看出,該網址格式很簡單,便於構造url。

接着,我們點擊下一頁按鈕,可以看到表格更新后,url沒有發生改變,可以判定是采用了Javscript。那么,我們首先判斷是不是采用了Ajax加載的。方法也很簡單,右鍵檢查或按F12,切換到network並選擇下面的XHR,再按F5刷新。可以看到只有一個Ajax請求,點擊下一頁也並沒有生成新的Ajax請求,可以判斷該網頁結構不是常見的那種點擊下一頁或者下拉會源源不斷出現的Ajax請求類型,那么便無法構造url來實現分頁爬取。

XHR選項里沒有找到我們需要的請求,接下來試試看能不能再JS里找到表格的數據請求。將選項選為JS,再次F5刷新,可以看到出現了很多JS請求,然后我們點擊幾次下一頁,會發現彈出新的請求來,然后右邊為響應的請求信息。url鏈接非常長,看上去很復雜。好,這里我們先在這里打住不往下了。

可以看到,通過分析后台元素來爬取該動態網頁的方法,相對比較復雜。那么有沒有干脆、直截了當地就能夠抓取表格內容的方法呢?有的,就是本文接下來要介紹的Selenium大法。

3. Selenium知識

Selenium 是什么?一句話,自動化測試工具。它是為了測試而出生的,但在近幾年火熱的爬蟲領域中,它搖身一變,變成了爬蟲的利器。直白點說, Seleninm能控制瀏覽器, 像人一樣”上網”。比如,可以實現網頁自動翻頁、登錄網站、發送郵件、下載圖片/音樂/視頻等等。舉個例子,寫幾行python代碼就可以用Selenium實現登錄IT桔子,然后瀏覽網頁的功能。

怎么樣,僅用幾行代碼就能實現自動上網操作,是不是挺神奇的?當然,這僅僅是Selenium最簡單的功能,還有很多更加豐富的操作,可以參考以下幾篇教程:

參考網站:

Selenium官網: https://selenium-python.readthedocs.io/

SeleniumPython文檔(英文版):http://selenium-python.readthedocs.org/index.html

SeleniumPython文檔(中文版):https://selenium-python-zh.readthedocs.io/en/latest/faq.html

Selenium 基本操作:https://www.yukunweb.com/2017/7/python-spider-Selenium-PhantomJS-basic/

Selenium爬取淘寶信息實戰:https://cuiqingcai.com/2852.html

只需要記住重要的一點就是:Selenium能做到"可見即可爬"。也就是說網頁上你能看到的東西,Selenium基本上都能爬取下來。包括上面我們提到的東方財富網的財務報表數據,它也能夠做到,而且非常簡單直接,不用去后台查看用了什么JavaScript技術或者Ajax參數。下面我們就實際來操練下吧。

4. 編碼實現

4.1. 思路

  • 安裝配置好Selenium運行的相關環境,瀏覽器可以用Chrome、Firefox、PhantomJS等,我用的是Chrome;
  • 東方財富網的財務報表數據不用登錄可直接獲得,Selenium更加方便爬取;
  • 先以單個網頁中的財務報表為例,表格數據結構簡單,可先直接定位到整個表格,然后一次性獲取所有td節點對應的表格單元內容;
  • 接着循環分頁爬取所有上市公司的數據,並保存為csv文件。
  • 重新構造靈活的url,實現可以爬取任意時期、任意一張財務報表的數據。

根據上述思路,下面就用代碼一步步來實現。

4.2. 爬取單頁表格

我們先以2018年中報的利潤表為例,抓取該網頁的第一頁表格數據,網頁url:http://data.eastmoney.com/bbsj/201806/lrb.html

快速定位到表格所在的節點:id = dt_1,然后可以用Selenium進行抓取了,方法如下:

from selenium import webdriver
browser = webdriver.Chrome()
# 當測試好能夠順利爬取后,為加快爬取速度可設置無頭模式,即不彈出瀏覽器
# 添加無頭headlesss 1使用chrome headless,2使用PhantomJS
# 使用 PhantomJS 會警告高不建議使用phantomjs,建議chrome headless
# chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument('--headless')
# browser = webdriver.Chrome(chrome_options=chrome_options)
# browser = webdriver.PhantomJS()
# browser.maximize_window() # 最大化窗口,可以選擇設置

browser.get('http://data.eastmoney.com/bbsj/201806/lrb.html')
element = browser.find_element_by_css_selector('#dt_1') # 定位表格,element是WebElement類型
# 提取表格內容td
td_content = element.find_elements_by_tag_name("td") # 進一步定位到表格內容所在的td節點
lst = [] # 存儲為list
for td in td_content:
lst.append(td.text)
print(lst) # 輸出表格內容

這里,使用Chrome瀏覽器構造一個Webdriver對象,賦值給變量browser,browser調用get()方法請求想要抓取的網頁。接着使用find_element_by_css_selector方法查找表格所在的節點:‘#dt_1’

這里推薦一款小巧、快速定位css/xpath的Chrome插件:SelectorGadget,使用這個插件就不用再去源代碼中手動定位節點那么麻煩了。

插件地址:https://chrome.google.com/webstore/detail/selectorgadget/mhjhnkcfbdhnjickkkdbjoemdmbfginb

緊接着再向下定位到td節點,因為網頁中有很多個td節點,所以要用find_elements方法。然后,遍歷數據節點存儲到list中。打印查看一下結果:

# list形式:
['1', '002161', '遠望谷', ...'-7960萬', '09-29',
'2','002316', '亞聯發展', ...'1.79億', '09-29',
'3',...
'50', '002683', '宏大爆破',...'1.37億', '09-01']

是不是很方便,幾行代碼就能抓取下來這一頁表格,除了速度有點慢。

為了便於后續存儲,我們將list轉換為DataFrame。首先需要把這一個大的list分割為多行多列的子list,實現如下:

import pandas as pd
# 確定表格列數
col = len(element.find_elements_by_css_selector('tr:nth-child(1) td'))
# 通過定位一行td的數量,可獲得表格的列數,然后將list拆分為對應列數的子list
lst = [lst[i:i + col] for i in range(0, len(lst), col)]
# 原網頁中打開"詳細"鏈接可以查看更詳細的數據,這里我們把url提取出來,方便后期查看
lst_link = []
links = element.find_elements_by_css_selector('#dt_1 a.red')
for link in links:
url = link.get_attribute('href')
lst_link.append(url)
lst_link = pd.Series(lst_link)
# list轉為dataframe
df_table = pd.DataFrame(lst)
# 添加url列
df_table['url'] = lst_link
print(df_table.head()) # 查看DataFrame

這里,要將list分割為子list,只需要確定表格有多少列即可,然后將每相隔這么多數量的值划分為一個子list。如果我們數一下該表的列數,可以發現一共有16列。但是這里不能使用這個數字,因為除了利潤表,其他報表的列數並不是16,所以當后期爬取其他表格可能就會報錯。這里仍然通過find_elements_by_css_selector方法,定位首行td節點的數量,便可獲得表格的列數,然后將list拆分為對應列數的子list。同時,原網頁中打開”詳細”列的鏈接可以查看更詳細的數據,這里我們把url提取出來,並增加一列到DataFrame中,方便后期查看。打印查看一下輸出結果:

可以看到,表格所有的數據我們都抓取到了,下面只需要進行分頁循環爬取就行了。

這里,沒有抓取表頭是因為表頭有合並單元格,處理起來就非常麻煩。建議表格抓取下來后,在excel中復制表頭進去就行了。如果,實在想要用代碼完成,可以參考這篇文章:https://blog.csdn.net/weixin_39461443/article/details/75456962

4.3. 分頁爬取

上面完成了單頁表格的爬取,下面我們來實現分頁爬取。

首先,我們先實現Selenium模擬翻頁跳轉操作,成功后再爬取每頁的表格內容。

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import time

browser = webdriver.Chrome()
browser.maximize_window() # 最大化窗口,可以選擇設置
wait = WebDriverWait(browser, 10)
def index_page(page):
try:
browser.get('http://data.eastmoney.com/bbsj/201806/lrb.html')
print('正在爬取第: %s 頁' % page)
wait.until(
EC.presence_of_element_located((By.ID, "dt_1")))
# 判斷是否是第1頁,如果大於1就輸入跳轉,否則等待加載完成。
if page > 1:
# 確定頁數輸入框
input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//*[@id="PageContgopage"]')))
input.click()
input.clear()
input.send_keys(page)
submit = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, '#PageCont > a.btn_link')))
submit.click()
time.sleep(2)
# 確認成功跳轉到輸入框中的指定頁
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, '#PageCont > span.at'), str(page)))
except Exception:
return None

def main():
for page in range(1,5): # 測試翻4頁
index_page(page)
if __name__ == '__main__':
main()

這里,我們先加載了相關包,使用WebDriverWait對象,設置最長10s的顯式等待時間,以便網頁加載出表格。判斷表格是否加載出來,用到了EC.presence_of_element_located條件。表格加載出來后,設置一個頁面判斷,如果在第1頁就等待頁面加載完成,如果大於第1頁就開始跳轉。

要完成跳轉操作,我們需要通過獲取輸入框input節點,然后用clear()方法清空輸入框,再通過send_keys()方法填寫相應的頁碼,接着通過submit.click()方法擊下一頁完成翻頁跳轉。

這里,我們測試一下前4頁跳轉效果,可以看到網頁成功跳轉了。下面就可以對每一頁應用第一頁爬取表格內容的方法,抓取每一頁的表格,轉為DataFrame然后存儲到csv文件中去。

http://media2.makcyun.top/selenium%E5%88%86%E9%A1%B5%E5%8E%8B%E7%BC%A9.gif

4.4. 通用爬蟲構造

上面,我們完成了2018年中報利潤表: http://data.eastmoney.com/bbsj/201806/lrb.html,一個網頁表格的爬取。但如果我們想爬取任意時期、任意一張報表的表格,比如2017年3季度的利潤表、2016年全年的業績報表、2015年1季度的現金流量表等等。上面的代碼就行不通了,下面我們對代碼進行一下改造,變成更通用的爬蟲。從圖中可以看到,東方財富網年報季報有7張表格,財務報表最早從2007年開始每季度一次。基於這兩個維度,可重新構造url的形式,然后爬取表格數據。下面,我們用代碼進行實現:

# 重構url
# 1 設置財務報表獲取時期
year = int(float(input('請輸入要查詢的年份(四位數2007-2018): ')))
# int表示取整,里面加float是因為輸入的是str,直接int會報錯,float則不會
while (year < 2007 or year > 2018):
year = int(float(input('年份數值輸入錯誤,請重新輸入:')))
quarter = int(float(input('請輸入小寫數字季度(1:1季報,2-年中報,3:3季報,4-年報): ')))
while (quarter < 1 or quarter > 4):
quarter = int(float(input('季度數值輸入錯誤,請重新輸入: ')))
# 轉換為所需的quarter 兩種方法,2表示兩位數,0表示不滿2位用0補充
quarter = '{:02d}'.format(quarter * 3)
# quarter = '%02d' %(int(month)*3)
date = '{}{}' .format(year, quarter)

# 2 設置財務報表種類
tables = int(
input('請輸入查詢的報表種類對應的數字(1-業績報表;2-業績快報表:3-業績預告表;4-預約披露時間表;5-資產負債表;6-利潤表;7-現金流量表): '))
dict_tables = {1: '業績報表', 2: '業績快報表', 3: '業績預告表',
4: '預約披露時間表', 5: '資產負債表', 6: '利潤表', 7: '現金流量表'}
dict = {1: 'yjbb', 2: 'yjkb/13', 3: 'yjyg',
4: 'yysj', 5: 'zcfz', 6: 'lrb', 7: 'xjll'}
category = dict[tables]

# 3 設置url
url = 'http://data.eastmoney.com/{}/{}/{}.html' .format('bbsj', date, category)
print(url) # 測試輸出的url

經過上面的設置,我們通過輸入想要獲得指定時期、制定財務報表類型的數值,就能返回相應的url鏈接。將該鏈接應用到前面的爬蟲中,就可以爬取相應的報表內容了。

另外,除了從第一頁開始爬取到最后一頁的結果以外,我們還可以自定義設置想要爬取的頁數。比如起始頁數從第1頁開始,然后爬取10頁。

# 4 選擇爬取頁數范圍
start_page = int(input('請輸入下載起始頁數:\n'))
nums = input('請輸入要下載的頁數,(若需下載全部則按回車):\n')
# 確定網頁中的最后一頁
browser.get(url)
# 確定最后一頁頁數不直接用數字而是采用定位,因為不同時間段的頁碼會不一樣
try:
page = browser.find_element_by_css_selector('.next+ a') # next節點后面的a節點
except:
page = browser.find_element_by_css_selector('.at+ a')
else:
print('沒有找到該節點')
# 上面用try.except是因為絕大多數頁碼定位可用'.next+ a',但是業績快報表有的只有2頁,無'.next+ a'節點
end_page = int(page.text)

if nums.isdigit():
end_page = start_page + int(nums)
elif nums == '':
end_page = end_page
else:
print('頁數輸入錯誤')
# 輸入准備下載表格類型
print('准備下載:{}-{}' .format(date, dict_tables[tables]))

經過上面的設置,我們就可以實現自定義時期和財務報表類型的表格爬取了,將代碼再稍微整理一下,可實現下面的爬蟲效果:

視頻截圖:

視頻地址:https://v.qq.com/x/page/y07335thsn2.html

背景中類似黑客帝國的代碼雨效果,其實是動態網頁效果。素材來源於下面這個網站,該網站還有很多酷炫的動態背景可以下載下來。

http://wallpaper.upupoo.com/store/paperDetail-1783830052.htm

4.5. 完整代碼

整個爬蟲的完整代碼如下所示:

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import time
import pandas as pd
import os

# 先chrome,后phantomjs
# browser = webdriver.Chrome()
# 添加無頭headlesss
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
browser = webdriver.Chrome(chrome_options=chrome_options)

# browser = webdriver.PhantomJS() # 會報警高提示不建議使用phantomjs,建議chrome添加無頭
browser.maximize_window() # 最大化窗口
wait = WebDriverWait(browser, 10)

def index_page(page):
try:
print('正在爬取第: %s 頁' % page)
wait.until(
EC.presence_of_element_located((By.ID, "dt_1")))
# 判斷是否是第1頁,如果大於1就輸入跳轉,否則等待加載完成。
if page > 1:
# 確定頁數輸入框
input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//*[@id="PageContgopage"]')))
input.click()
input.clear()
input.send_keys(page)
submit = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, '#PageCont > a.btn_link')))
submit.click()
time.sleep(2)
# 確認成功跳轉到輸入框中的指定頁
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, '#PageCont > span.at'), str(page)))
except Exception:
return None

def parse_table():
# 提取表格第一種方法
# element = wait.until(EC.presence_of_element_located((By.ID, "dt_1")))
# 第二種方法
element = browser.find_element_by_css_selector('#dt_1')

# 提取表格內容td
td_content = element.find_elements_by_tag_name("td")
lst = []
for td in td_content:
# print(type(td.text)) # str
lst.append(td.text)

# 確定表格列數
col = len(element.find_elements_by_css_selector('tr:nth-child(1) td'))
# 通過定位一行td的數量,可獲得表格的列數,然后將list拆分為對應列數的子list
lst = [lst[i:i + col] for i in range(0, len(lst), col)]

# 原網頁中打開"詳細"鏈接,可以查看更詳細的數據,這里我們把url提取出來,方便后期查看
lst_link = []
links = element.find_elements_by_css_selector('#dt_1 a.red')
for link in links:
url = link.get_attribute('href')
lst_link.append(url)

lst_link = pd.Series(lst_link)
# list轉為dataframe
df_table = pd.DataFrame(lst)
# 添加url列
df_table['url'] = lst_link

# print(df_table.head())
return df_table

# 寫入文件
def write_to_file(df_table, category):
# 設置文件保存在D盤eastmoney文件夾下
file_path = 'D:\\eastmoney'
if not os.path.exists(file_path):
os.mkdir(file_path)
os.chdir(file_path)
df_table.to_csv('{}.csv' .format(category), mode='a',
encoding='utf_8_sig', index=0, header=0)

# 設置表格獲取時間、類型
def set_table():
print('*' * 80)
print('\t\t\t\t東方財富網報表下載')
print('作者:高級農民工 2018.10.6')
print('--------------')

# 1 設置財務報表獲取時期
year = int(float(input('請輸入要查詢的年份(四位數2007-2018):\n')))
# int表示取整,里面加float是因為輸入的是str,直接int會報錯,float則不會 # https://stackoverflow.com/questions/1841565/valueerror-invalid-literal-for-int-with-base-10 while (year < 2007 or year > 2018): year = int(float(input('年份數值輸入錯誤,請重新輸入:\n'))) quarter = int(float(input('請輸入小寫數字季度(1:1季報,2-年中報,3:3季報,4-年報):\n'))) while (quarter < 1 or quarter > 4): quarter = int(float(input('季度數值輸入錯誤,請重新輸入:\n'))) # 轉換為所需的quarter 兩種方法,2表示兩位數,0表示不滿2位用0補充, # http://www.runoob.com/python/att-string-format.html quarter = '{:02d}'.format(quarter * 3) # quarter = '%02d' %(int(month)*3) date = '{}{}' .format(year, quarter) # print(date) 測試日期 ok # 2 設置財務報表種類 tables = int( input('請輸入查詢的報表種類對應的數字(1-業績報表;2-業績快報表:3-業績預告表;4-預約披露時間表;5-資產負債表;6-利潤表;7-現金流量表): \n')) dict_tables = {1: '業績報表', 2: '業績快報表', 3: '業績預告表', 4: '預約披露時間表', 5: '資產負債表', 6: '利潤表', 7: '現金流量表'} dict = {1: 'yjbb', 2: 'yjkb/13', 3: 'yjyg', 4: 'yysj', 5: 'zcfz', 6: 'lrb', 7: 'xjll'} category = dict[tables] # 3 設置url # url = 'http://data.eastmoney.com/bbsj/201803/lrb.html' eg. url = 'http://data.eastmoney.com/{}/{}/{}.html' .format( 'bbsj', date, category) # # 4 選擇爬取頁數范圍 start_page = int(input('請輸入下載起始頁數:\n')) nums = input('請輸入要下載的頁數,(若需下載全部則按回車):\n') print('*' * 80) # 確定網頁中的最后一頁 browser.get(url) # 確定最后一頁頁數不直接用數字而是采用定位,因為不同時間段的頁碼會不一樣 try: page = browser.find_element_by_css_selector('.next+ a') # next節點后面的a節點 except: page = browser.find_element_by_css_selector('.at+ a') # else: # print('沒有找到該節點') # 上面用try.except是因為絕大多數頁碼定位可用'.next+ a',但是業績快報表有的只有2頁,無'.next+ a'節點 end_page = int(page.text) if nums.isdigit(): end_page = start_page + int(nums) elif nums == '': end_page = end_page else: print('頁數輸入錯誤') # 輸入准備下載表格類型 print('准備下載:{}-{}' .format(date, dict_tables[tables])) print(url) yield{ 'url': url, 'category': dict_tables[tables], 'start_page': start_page, 'end_page': end_page }def main(category, page): try: index_page(page) # parse_table() #測試print df_table = parse_table() write_to_file(df_table, category) print('第 %s 頁抓取完成' % page) print('--------------') except Exception: print('網頁爬取失敗,請檢查網頁中表格內容是否存在')# 單進程if __name__ == '__main__': for i in set_table(): # url = i.get('url') category = i.get('category') start_page = i.get('start_page') end_page = i.get('end_page') for page in range(start_page, end_page): # for page in range(44,pageall+1): # 如果下載中斷,可以嘗試手動更改網頁繼續下載 main(category, page) print('全部抓取完成')

這里,我下載了所有上市公司的部分報表。

2018年中報業績報表:

2017年報的利潤表:

如果你想下載更多的報表,可以使用文中的代碼,代碼和素材資源可以在下面的鏈接中獲取:

https://github.com/makcyun/eastmoney_spider

另外,爬蟲還可以再完善一下,比如增加爬取上市公司的公告信息,設置可以爬任意一家(數家/行業)的公司數據而不用全部。

還有一個問題是,Selenium爬取的速度很慢而且很占用內存,建議盡量先嘗試采用Requests請求的方法,抓不到的時候再考慮這個。文章開頭在進行網頁分析的時候,我們初步分析了表格JS的請求數據,是否能從該請求中找到我們需要的表格數據呢? 后續文章,我們換一個思路再來嘗試爬取一次。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM