過基本的 Python 工具獲得爬取完整 HTML 網站的實踐經驗。
? 來源:linux.cn ? 作者:Julia Piaskowski ? 譯者:Hacker ?
(本文字數:11235,閱讀時長大約:14 分鐘)
有很多很棒的書可以幫助你學習 Python ,但是誰真正讀了這那些大部頭呢?(劇透:反正不是我)。
許多人覺得教學書籍很有用,但我通常不會從頭到尾地閱讀一本書來學習。我是通過做一個項目,努力的弄清楚一些內容,然后再讀另一本書來學習。因此,暫時丟掉書,讓我們一起學習 Python。
接下來是我的第一個 Python 爬取項目的指南。它對 Python 和 HTML 的假定知識要求很低。這篇文章旨在說明如何使用 Python 的 requests 庫訪問網頁內容,并使用 BeatifulSoup4 庫以及 JSON 和 pandas 庫解析網頁內容。我將簡要介紹 Selenium 庫,但我不會深入研究如何使用該庫——這個主題值得有自己的教程。最終,我希望向你展示一些技巧和小竅門,以減少網頁爬取過程中遇到的問題。
我的 GitHub 存儲庫 中提供了本指南的所有資源。如果需要安裝 Python3 的幫助,請查看 Linux 、 Windows 和 Mac 的教程。
$ python3 -m venv
$ source venv/bin/activate
$ pip install requests bs4 pandas
如果你喜歡使用 JupyterLab ,則可以使用 notebook 運行所有代碼。 安裝 JupyterLab 有很多方法,這是其中一種:
# from the same virtual environment as above, run:
$ pip install jupyterlab
現在我們已經安裝了依賴項,但是爬取網頁需要做什么?
讓我們退一步,確保使目標清晰。下面是成功完成網頁爬取項目需求列表:
關于 HTML 的備注:HTML 是運行在互聯網上的“猛獸”,但我們最需要了解的是標簽的工作方式。標簽是一對由尖括號包圍關鍵詞(一般成對出現,其內容在兩個標簽中間)。比如,這是一個假裝的標簽,稱為 pro-tip:
<pro-tip> All you need to know about html is how tags work </pro-tip>
我們可以通過調用標簽 pro-tip 來訪問其中的信息(All you need to know…)。本教程將進一步介紹如何查找和訪問標簽。要進一步了解 HTML 基礎知識,請查看 本文 。
有些數據利用網站爬取采集比利用其他方法更合適。以下是我認為合適項目的準則:
沒有可用于數據(處理)的公共 API。通過 API 抓取結構化數據會容易得多,(所以沒有 API )有助于澄清收集數據的合法性和道德性。而有相當數量的結構化數據,并有規律的、可重復的格式,才能證明這種努力的合理性。網頁爬取可能會很痛苦。BeautifulSoup(bs4)使操作更容易,但無法避免網站的個別特殊性,需要進行定制。數據的相同格式化不是必須的,但這確實使事情變得更容易。存在的 “邊際案例”(偏離規范)越多,爬取就越復雜。
免責聲明:我沒有參加過法律培訓;以下內容無意作為正式的法律建議。
關于合法性,訪問大量有價值信息可能令人興奮,但僅僅因為它是可能的,并不意味著應該這樣做。
值得慶幸的是,有一些公共信息可以指導我們的道德規范和網頁爬取工具。大多數網站都有與該網站關聯的 robots.txt 文件,指出允許哪些爬取活動,哪些不被允許。它主要用于與搜索引擎(網頁抓取工具的終極形態)進行交互。然而,網站上的許多信息都被視為公共信息。因此,有人將 robots.txt 文件視為一組建議,而不是具有法律約束力的文檔。 robots.txt 文件并不涉及數據的道德收集和使用等主題。
在開始爬取項目之前,問自己以下問題:
當我爬取一個網站時,請確??梢詫λ羞@些問題回答 “否”。
要深入了解這些法律問題,請參閱 2018 年出版的 Krotov 和 Silva 撰寫的 《Web 爬取的合法性和道德性》 和 Sellars 的 《二十年 Web 爬取和計算機欺詐與濫用法案》 。
經過上述評估,我想出了一個項目。我的目標是爬取愛達荷州所有 Family Dollar 商店的地址。 這些商店在農村地區規模很大,因此我想了解有多少家這樣的商店。
起點是 Family Dollar 的位置頁面
愛達荷州 Family Dollar 所在地頁面
首先,讓我們在 Python 虛擬環境中加載先決條件。此處的代碼將被添加到一個 Python 文件(如果你想要個名稱,則為 scraper.py)或在 JupyterLab 的單元格中運行。
import requests # for making standard html requests
from bs4 import BeautifulSoup # magical tool for parsing html data
import json # for parsing data
from pandas import DataFrame as df # premier library for data organization
接下來,我們從目標 URL 中請求數據。
page=requests.get("https://locations.familydollar.com/id/")
soup=BeautifulSoup(page.text, 'html.parser')
BeautifulSoup 將 HTML 或 XML 內容轉換為復雜樹對象。這是我們將使用的幾種常見對象類型。
當我們查看 requests.get() 輸出時,還有更多要考慮的問題。我僅使用 page.text() 將請求的頁面轉換為可讀的內容,但是還有其他輸出類型:
我只在使用拉丁字母的純英語網站上操作。 requests 中的默認編碼設置可以很好地解決這一問題。然而,除了純英語網站之外,就是更大的互聯網世界。為了確保 requests 正確解析內容,你可以設置文本的編碼:
page=requests.get(URL)
page.encoding='ISO-885901'
soup=BeautifulSoup(page.text, 'html.parser')
仔細研究 BeautifulSoup 標簽,我們看到:
警告:此過程可能令人沮喪。
網站爬取過程中的提取可能是一個令人生畏的充滿了誤區的過程。我認為解決此問題的最佳方法是從一個有代表性的示例開始然后進行擴展(此原理對于任何編程任務都是適用的)。查看頁面的 HTML 源代碼至關重要。有很多方法可以做到這一點。
你可以在終端中使用 Python 查看頁面的整個源代碼(不建議使用)。運行此代碼需要你自擔風險:
print(soup.prettify())
雖然打印出頁面的整個源代碼可能適用于某些教程中顯示的玩具示例,但大多數現代網站的頁面上都有大量內容。甚至 404 頁面也可能充滿了頁眉、頁腳等代碼。
通常,在你喜歡的瀏覽器中通過 “查看頁面源代碼” 來瀏覽源代碼是最容易的(單擊右鍵,然后選擇 “查看頁面源代碼” )。這是找到目標內容的最可靠方法(稍后我將解釋原因)。
Family Dollar 頁面源代碼
在這種情況下,我需要在這個巨大的 HTML 海洋中找到我的目標內容 —— 地址、城市、州和郵政編碼。通常,對頁面源(ctrl+F)的簡單搜索就會得到目標位置所在的位置。一旦我實際看到目標內容的示例(至少一個商店的地址),便會找到將該內容與其他內容區分開的屬性或標簽。
首先,我需要在愛達荷州 Family Dollar 商店中收集不同城市的網址,并訪問這些網站以獲取地址信息。這些網址似乎都包含在 href 標記中。太棒了!我將嘗試使用 find_all 命令進行搜索:
dollar_tree_list=soup.find_all('href')
dollar_tree_list
搜索 href 不會產生任何結果,該死。這可能是因為 href 嵌套在 itemlist 類中而失敗。對于下一次嘗試,請搜索 item_list。由于 class 是 Python 中的保留字,因此使用 class_ 來作為替代。soup.find_all() 原來是 bs4 函數的瑞士軍刀。
dollar_tree_list=soup.find_all(class_='itemlist')
for i in dollar_tree_list[:2]:
print(i)
有趣的是,我發現搜索一個特定類的方法一般是一種成功的方法。通過找出對象的類型和長度,我們可以了解更多有關對象的信息。
type(dollar_tree_list)
len(dollar_tree_list)
可以使用 .contents 從 BeautifulSoup “結果集” 中提取內容。這也是創建單個代表性示例的好時機。
example=dollar_tree_list[2] # a representative example
example_content=example.contents
print(example_content)
使用 .attr 查找該對象內容中存在的屬性。注意:.contents 通常會返回一個項目的精確的列表,因此第一步是使用方括號符號為該項目建立索引。
example_content=example.contents[0]
example_content.attrs
現在,我可以看到 href 是一個屬性,可以像字典項一樣提取它:
example_href=example_content['href']
print(example_href)
所有的這些探索為我們提供了前進的路徑。這是厘清上面邏輯的一個清理版本。
city_hrefs=[] # initialise empty list
for i in dollar_tree_list:
cont=i.contents[0]
href=cont['href']
city_hrefs.append(href)
# check to be sure all went well
for i in city_hrefs[:2]:
print(i)
輸出的內容是一個關于抓取愛達荷州 Family Dollar 商店 URL 的列表。
也就是說,我仍然沒有獲得地址信息!現在,需要抓取每個城市的 URL 以獲得此信息。因此,我們使用一個具有代表性的示例重新開始該過程。
page2=requests.get(city_hrefs[2]) # again establish a representative example
soup2=BeautifulSoup(page2.text, 'html.parser')
Family Dollar 地圖和代碼
地址信息嵌套在 type="application/ld+json" 里。經過大量的地理位置抓取之后,我開始認識到這是用于存儲地址信息的一般結構。幸運的是,soup.find_all() 開啟了利用 type 搜索。
arco=soup2.find_all(type="application/ld+json")
print(arco[1])
地址信息在第二個列表成員中!原來如此!
使用 .contents 提?。◤牡诙€列表項中)內容(這是過濾后的合適的默認操作)。同樣,由于輸出的內容是一個列表,因此我為該列表項建立了索引:
arco_contents=arco[1].contents[0]
arco_contents
喔,看起來不錯。此處提供的格式與 JSON 格式一致(而且,該類型的名稱中確實包含 “json”)。 JSON 對象的行為就像是帶有嵌套字典的字典。一旦你熟悉利用其去工作,它實際上是一種不錯的格式(當然,它比一長串正則表達式命令更容易編程)。盡管從結構上看起來像一個 JSON 對象,但它仍然是 bs4 對象,需要通過編程方式轉換為 JSON 對象才能對其進行訪問:
arco_json= json.loads(arco_contents)
type(arco_json)
print(arco_json)
在該內容中,有一個被調用的 address 鍵,該鍵要求地址信息在一個比較小的嵌套字典里。可以這樣檢索:
arco_address=arco_json['address']
arco_address
好吧,請大家注意。現在我可以遍歷存儲愛達荷州 URL 的列表:
locs_dict=[] # initialise empty list
for link in city_hrefs:
locpage=requests.get(link) # request page info
locsoup=BeautifulSoup(locpage.text, 'html.parser')
# parse the page's content
locinfo=locsoup.find_all(type="application/ld+json")
# extract specific element
loccont=locinfo[1].contents[0]
# get contents from the bs4 element set
locjson=json.loads(loccont) # convert to json
locaddr=locjson['address'] # get address
locs_dict.append(locaddr) # add address to list
我們在字典中裝載了大量數據,但是還有一些額外的無用項,它們會使重用數據變得比需要的更為復雜。要執行最終的數據組織,我們需要將其轉換為 Pandas 數據框架,刪除不需要的列 @type 和 country,并檢查前五行以確保一切正常。
locs_df=df.from_records(locs_dict)
locs_df.drop(['@type', 'addressCountry'], axis=1, inplace=True)
locs_df.head(n=5)
確保保存結果??!
df.to_csv(locs_df, "family_dollar_ID_locations.csv", sep=",", index=False)
我們做到了!所有愛達荷州 Family Dollar 商店都有一個用逗號分隔的列表。多令人興奮。
Selenium 是用于與網頁自動交互的常用工具。為了解釋為什么有時必須使用它,讓我們來看一個使用 Walgreens 網站的示例。 “檢查元素” 提供了瀏覽器顯示內容的代碼:
雖然 “查看頁面源代碼” 提供了有關 requests 將獲得什么內容的代碼:
如果這兩個不一致,是有一些插件可以修改源代碼 —— 因此,應在將頁面加載到瀏覽器后對其進行訪問。requests 不能做到這一點,但是 Selenium 可以做到。
Selenium 需要 Web 驅動程序來檢索內容。實際上,它會打開 Web 瀏覽器,并收集此頁面的內容。Selenium 功能強大 —— 它可以通過多種方式與加載的內容進行交互(請閱讀文檔)。使用 Selenium 獲取數據后,繼續像以前一樣使用 BeautifulSoup:
url="https://www.walgreens.com/storelistings/storesbycity.jsp?requestType=locator&state=ID"
driver=webdriver.Firefox(executable_path='mypath/geckodriver.exe')
driver.get(url)
soup_ID=BeautifulSoup(driver.page_source, 'html.parser')
store_link_soup=soup_ID.find_all(class_='col-xl-4 col-lg-4 col-md-4')
對于 Family Dollar 這種情形,我不需要 Selenium,但是當呈現的內容與源代碼不同時,我確實會保留使用 Selenium。
總之,使用網站抓取來完成有意義的任務時:
如果你對答案感到好奇:
Family Dollar 位置圖
美國有很多 Family Dollar 商店。
完整的源代碼是:
import requests
from bs4 import BeautifulSoup
import json
from pandas import DataFrame as df
page=requests.get("https://www.familydollar.com/locations/")
soup=BeautifulSoup(page.text, 'html.parser')
# find all state links
state_list=soup.find_all(class_='itemlist')
state_links=[]
for i in state_list:
cont=i.contents[0]
attr=cont.attrs
hrefs=attr['href']
state_links.append(hrefs)
# find all city links
city_links=[]
for link in state_links:
page=requests.get(link)
soup=BeautifulSoup(page.text, 'html.parser')
familydollar_list=soup.find_all(class_='itemlist')
for store in familydollar_list:
cont=store.contents[0]
attr=cont.attrs
city_hrefs=attr['href']
city_links.append(city_hrefs)
# to get individual store links
store_links=[]
for link in city_links:
locpage=requests.get(link)
locsoup=BeautifulSoup(locpage.text, 'html.parser')
locinfo=locsoup.find_all(type="application/ld+json")
for i in locinfo:
loccont=i.contents[0]
locjson=json.loads(loccont)
try:
store_url=locjson['url']
store_links.append(store_url)
except:
pass
# get address and geolocation information
stores=[]
for store in store_links:
storepage=requests.get(store)
storesoup=BeautifulSoup(storepage.text, 'html.parser')
storeinfo=storesoup.find_all(type="application/ld+json")
for i in storeinfo:
storecont=i.contents[0]
storejson=json.loads(storecont)
try:
store_addr=storejson['address']
store_addr.update(storejson['geo'])
stores.append(store_addr)
except:
pass
# final data parsing
stores_df=df.from_records(stores)
stores_df.drop(['@type', 'addressCountry'], axis=1, inplace=True)
stores_df['Store']="Family Dollar"
df.to_csv(stores_df, "family_dollar_locations.csv", sep=",", index=False)
作者注釋:本文改編自 2020 年 2 月 9 日在俄勒岡州波特蘭的 我在 PyCascades 的演講 。
via: opensource.com
作者: Julia Piaskowski 選題: lujun9972 譯者: stevenzdg988 校對: wxy
本文由 LCTT 原創編譯, Linux中國 榮譽推出
TMl 的標簽可以分為單個標簽和成對標簽。
單個標簽:html4 規定單個標簽要有一個 / 表示結尾, html5 則不用
<!--單個標簽-->
<meta>
<!--成對標簽 -->
<div></div>
以下是HTMl中常用的一些標簽
div 標簽 主要用來將相關的內容組合到一塊,就像菜市場把各個蔬菜分成不同種類區分擺放是一個道理。
div 是最常見也是比較重要的標簽,網頁布局中經常使用的一類標簽。通常布局被稱為 DIV + CSS 布局
<div>
div 就是一個分類的存儲箱子
</div>
p標簽表示段落, 在網頁文字中應用的比較多
<!--段落和段落間會換行-->
<p>第一段</p>
<p>第二段</p>
h標簽分為六個
標簽 | 語義 |
h1 | 一級標題 |
h2 | 二級標題 |
h3 | 三級標題 |
h4 | 四級標題 |
h5 | 五級標題 |
h6 | 六級標題 |
引用標題標簽后,字體會加粗、字號一會變大
無序標簽是沒有顯示順序的列表,無序列表前面通常會有一個“小點”, 這個小點可以用type屬性控制。其中有三個展示方式(不過這種方式比較固定,不夠靈活和美觀, 已經被CSS的效果代替),如下:
值(type屬性) | 描述 |
disc | 默認值,實心圓 |
circle | 空心圓 |
square | 實心方框 |
舉例:
<!--ul標簽內部只能放置li標簽-->
<!--li標簽內部可以放其他的標簽-->
<ul type=">
<li>無序列表元素1</li> <!--列表項-->
<li>無序列表元素2</li>
</ul>
實心圓
<ul type="disc">
<li>西紅柿</li>
<li>黃瓜</li>
</ul>
空心圓
<ul type="circle">
<li>西紅柿</li>
<li>黃瓜</li>
</ul>
實心方框
<ul type="square">
<li>西紅柿</li>
<li>黃瓜</li>
</ul>
type屬性值 | 意義 |
a | 小寫英文字母編號 |
A | 大寫英文字母編號 |
i | 小寫羅馬數字編號 |
I | 大寫羅馬數字編號 |
1 | 數字編號(默認) |
有序列表, 從2開始
<ol start="2">
<li>元素1</li>
<li>元素2</li>
</ol>
小寫字母表示
<ol type="a">
<li>元素1</li>
<li>元素2</li>
<li>元素3</li>
</ol>
倒敘
<ol reversed>
<li>元素1</li>
<li>元素2</li>
<li>元素3</li>
</ol>
dl標簽表示自定義列表
dt表示數據項,dd表示數據定義, dd是dt標簽的解釋
<dl>
<dt>西紅柿</dt>
<dd>紅、酸</dd>
<dt>黃瓜</dt>
<dd>綠、澀</dd>
</dl>
img 用來插入圖片,包括但不限于以下圖片格式
圖片格式 | 備注 |
.jpg、.jpeg | 通常用于照片,是一種有損壓縮格式 |
.png | 通常用于logo、背景,支持透明和半透明。便攜式網絡圖像 |
.svg | 矢量圖片 |
<!-- src(source)屬性, 圖片地址,可以為相對路徑,也可以為絕對路徑-->
<!-- alt 如果遇到圖片無法加載的情況,網頁上會展示 alt的 值 -->
<!-- width 和 height 表示 寬和高, 如果只設置一個, 那么另外一個就會跟著成比例縮放-->
<img src="./images/images.jpg" alt="星期一" width="120" height="20">
用a標簽來制作超級鏈接
<!-- href 屬性 表示 其他頁面的鏈接,支持相對路徑和絕對路徑,還可以鏈接到其它網站 -->
<!--target 屬性表示 打開其他鏈接的方式-->
<!-- title 屬性表示 鏈接的標題, 當鼠標移動到鏈接上,會展示出來-->
<a href="http://www.baidu.com" target="blank" title="文字標題">百度</a>
<!--也可以用a標簽作為錨點 錨點可以是本頁面的錨點,也可以是其他頁面的錨點-->
<h1 id="title">頭部標題</h1>
... 此處省略一些代碼
<a href="#title">返回標題</a>
<!--下載鏈接,指向 doc, zip, zip等文件格式時,a標簽將成為自動下載鏈接-->
<a href="./download/halou.zip">發郵件</a>
<!-- mailto:前綴的鏈接是郵件鏈接,系統將自動打開email相關軟件-->
<a href="mailto:halouworld@126.com">發郵件</a>
<!-- tel: 前綴鏈接是電話鏈接,系統將自動打開撥號鍵-->
<a href="tel:11111111111">打開撥號鍵盤</a>
audio標簽用來插入音頻標簽
<!--添加 controls 后才會顯示 播放控件-->
<!--常用音頻格式 mp3 和 ogg格式-->
<!--autoplay 自動播放屬性-->
<!--loop 屬性表示循環播放-->
<audio controls src="./video/demo.mp3">
您的瀏覽器不支持 audio標簽,請升級
</audio>
<audio controls src="./video/demo.mp3" autoplay loop>
您的瀏覽器不支持 audio標簽,請升級
</audio>
video 標簽用于插入一段視頻
<!--有的視頻不能播放 ,詳見 https://blog.csdn.net/weixin_34272308/article/details/94614657 -->
<!-- controls 顯示視頻播放控件 -->
<!-- autoplay 自動播放 -->
<!-- loop 循環播放 -->
<!-- 常見的 視頻格式 mp4 ogv webm 等-->
<video controls autoplay loop src="./video/5-4 RDB2.mp4" >
您的瀏覽器不支持 video標簽,請升級
</video>
以前的區塊標簽只有div,現在為了更好的方便搜索引擎抓取網站,因此有了以下語義更加明確的區塊標簽
<section> | 文檔的區域,比div語義上還要大一點 |
<header> | 頁頭 |
<main> | 網頁核心部分 |
<footer> | 頁腳 |
表單用來收集信息并且可以完成和后端的數據傳輸
表單中大致可以分為三種標簽
一些表單的示例
<!--action 表示要提交到后端的網址-->
<!--method 表示表單提交的方式,通常有 get 、 post 、put、delete等-->
<form action="/save" meththo="post"></form>
<!--<form> 標簽中 input 文本框 type="text" 表示文本框-->
<!-- value 表示文本框中的值 -->
<!--planceholder表示提示文字,在沒任何輸入值的情況下,作為提示信息-->
<!--disabled 表示禁用-->
<input type="text" value="123" planceholder="提示文字" disabled>
<!---單選按鈕,name相等,表示選擇了一個,另一個就不能選擇了-->
<!--checked 表示默認被選中-->
<!-- value 屬性表示要提交到后端服務器的值-->
<input type="radio" name="radio_group" checked>
<input type="radio" name="radio_group">
<label>
<input type="radio" name="sex"> 男
</label>
<label>
<input type="radio" name="sex"> 女
</label>
<!--html4 中的標簽 通過for 屬性 和 其他標簽的id屬性進行綁定-->
<input type="radio" name="sex" id="nan">
<label for="nan">男</label>
<input type="radio" name="sex" id="nv">
<label for="nv">女</label>
<!--復選框 type="checkbox" 同一組的的復選框,name值應該相同 ,復選框也有value值, 用于向服務器提交數據-->
<input type="checkbox" name="hobby" value="soccer" > 足球
<input type="checkbox" name="hobby" value="basket" > 籃球
<!--密碼框-->
<input type="password" placeholder="請輸入密碼">
<!-- 下拉菜單 -->
<select>
<option value="alipay">支付寶</option>
<option value="wxpay">微信支付</option>
</select>
<!--多文本框 rows 和 clos 分別用于設置 行數 和 列數-->
<textarea rows="3" cols="5"></textarea>
<!--三種按鈕 submit 提交按鈕 button 普通按鈕 可以簡寫為 <button></button> reset 按鈕 重置按鈕-->
<input type="button" value="普通按鈕">
<input type="reset" value="重置按鈕">
<input type="submit" value="提交表單">
<!--像 email 和 url 等格式,如果點擊提交按鈕,不符合格式,會有提示-->
<form>
日期空間: <input type="date"> <br/>
時間空間: <input type="time"> <br/>
日期時間空間 <input type="datetime-local"> <br/>
文件:<input type="file"> <br/> <br/>
數字控件: <input type="number"> <br/>
拖拽條: <input type="range"> <br/>
搜索框: <input type="search"> <br/>
網址控件: <input type="url"> <br/>
郵箱控件: <input type="email" >
<input type="submit" value="提交">
</form>
<!-- datalist 備選項示例 -->
<input type="text" list="province">
<datalist id="province">
<option value="陜西"></option>
<option value="山西"></option>
<option value="河北"></option>
<option value="山東"></option>
</datalist>
可以用html渲染表格
<!--表格示例-->
<table border="1">
<caption>我是標題</caption>
<tr>
<th>第一列標題</th>
<th>第二列標題</th>
</tr>
<tr>
<td>第一行第一列</td>
<td>第一行第二列</td>
</tr>
<tr>
<td>第二行第一列</td>
<td>第二行第二列</td>
</tr>
</table>
<!--跨列示例-->
<table border="1">
<caption>我是標題</caption>
<tr>
<th>第一列標題</th>
<th>第二列標題</th>
</tr>
<tr>
<td colspan="2">跨兩行</td>
</tr>
<tr>
<td>第二行第一列</td>
<td>第二行第二列</td>
</tr>
</table>
<!--跨行示例-->
<table border="1">
<caption>我是標題</caption>
<tr>
<th>第一列標題</th>
<th>第二列標題</th>
</tr>
<tr>
<td rowspan="2">第一行第一列</td>
<td>第一行第二列</td>
</tr>
<tr>
<td>第二行第二列</td>
</tr>
<tr>
<td>第三行第一列</td>
<td>第三行第二列</td>
</tr>
</table>
markdown中寫下你的文章,并使用Python將它們轉換成HTML-作者Florian Dahlitz,于2020年5月18日(15分鐘)
介紹
幾個月前,我想開通自己的博客,而不是使用像Medium這樣的網站。這是一個非?;A的博客,所有的文章都是HTML形式的。然而,有一天,我突然產生了自己編寫Markdown到HTML生成器的想法,最終這將允許我用markdown來編寫文章。此外,為它添加諸如估計閱讀時間之類的擴展特性會更容易。長話短說,我實現了自己的markdown到HTML生成器,我真的很喜歡它!
在本系列文章中,我想向您展示如何構建自己的markdown到HTML生成器。該系列由三部分組成:
第一部分(本文)介紹了整個管線的實現。
第二部分通過一個模塊擴展了實現的管線,該模塊用于計算給定文章的預計閱讀時間。
第三部分演示如何使用管線生成自己的RSS摘要。
這三部分中使用的代碼都可以在GitHub上找到。
備注:我的文章中markdown到HTML生成器的想法基于Anthony Shaw文章中的實現。
項目構建
為了遵循本文的內容,您需要安裝幾個軟件包。我們把它們放進requirements.txt文件。
Markdown是一個包,它允許您將markdown代碼轉換為HTML。之后我們用Flask產生靜態文件。
但在安裝之前,請創建一個虛擬環境,以避免Python安裝出現問題:
激活后,您可以使用pip安裝requirements.txt中的依賴。
很好!讓我們創建幾個目錄來更好地組織代碼。首先,我們創建一個app目錄。此目錄包含我們提供博客服務的Flask應用程序。所有后續目錄都將在app目錄內創建。其次,我們創建一個名為posts的目錄。此目錄包含要轉換為HTML文件的markdown文件。接下來,我們創建一個templates目錄,其中包含稍后使用Flask展示的模板。在templates目錄中,我們再創建兩個目錄:
posts包含生成的HTML文件,這些文件與應用程序根目錄中posts目錄中的文件相對應。
shared包含在多個文件中使用的HTML文件。
此外,我們還創建了一個名為services的目錄。該目錄將包含我們在Flask應用程序中使用的模塊,或者為它生成某些東西。最后,創建一個名為static的目錄帶有兩個子目錄images和css。自定義CSS文件和文章的縮略圖將存儲在此處。
您的最終項目結構應如下所示:
令人驚嘆!我們完成了一般的項目設置。我們來看看Flask的設置。
Flask設置
路由
我們在上一節安裝了Flask。但是,我們仍然需要一個Python文件來定義用戶可以訪問的端點。在app目錄中創建main.py并將以下內容復制到其中。
該文件定義了一個具有兩個端點的基礎版Flask應用程序。用戶可以使用/route訪問第一個端點返回索引頁,其中列出了所有文章。
第二個端點是更通用的端點。它接受post的名稱并返回相應的HTML文件。
接下來,我們通過向app目錄中添加一個__init__.py,將其轉換為一個Python包。此文件為空。如果您使用UNIX計算機,則可以從項目的根目錄運行以下命令:
模板
現在,我們創建兩個模板文件index.html以及layout.html,都存儲在templates/shared目錄中。這個layout.html模板將用于單個博客條目,而index.html模板用于生成索引頁,從中我們可以訪問每個帖子。讓我們從index.html模板開始。
它是一個基本的HTML文件,其中有兩個元標記、一個標題和兩個樣式表。注意,我們使用一個遠程樣式表和一個本地樣式表。遠程樣式表用于啟用Bootstrap[1]類。第二個是自定義樣式。我們晚點再定義它們。
HTML文件的主體包含一個容器,其中包含Jinja2[2]邏輯,用于為每個post生成Bootstrap卡片[3]。您是否注意到我們不直接基于變量名訪問這些值,而是需要將[0]添加到其中?這是因為文章中解析的元數據是列表。實際上,每個元數據元素都是由單一元素組成的列表。我們稍后再看。到目前為止,還不錯。讓我們看看layout.html模板。
如你所見,它比前一個短一點,簡單一點。文件頭與index.html文件很相似,除了我們有不同的標題。當然,我們可以共用一個模板,但是我不想讓事情變得更復雜。
body中的容器僅定義一個h1標記。然后,我們提供給模板的內容被插入并呈現。
樣式
正如上一節所承諾的,我們將查看自定義CSS文件style.css. 我們在static/css中找到該文件,并根據需要自定義頁面。下面是我們將用于基礎示例的內容:
我不喜歡Bootstrap中blockquotes的默認外觀,所以我們在左側添加了一點間距和邊框。此外,blockquote段落底部的頁邊空白將被刪除。不刪除的話看起來很不自然。
最后但并非最不重要的是,左右兩邊的填充被刪除。由于兩邊都有額外的填充,縮略圖沒有正確對齊,所以在這里刪除它們。
到現在為止,一直都還不錯。我們完成了關于Flask的所有工作。讓我們開始寫一些帖子吧!
寫文章
正如標題所承諾的,你可以用markdown寫文章-是的!在寫文章的時候,除了保證正確的markdown格式外,沒有其他需要注意的事情。
在完成本文之后,我們需要在文章中添加一些元數據。此元數據添加在文章之前,并由三個破折號分隔開來---。下面是一個示例文章(post1.md)的摘錄:
注意:您可以在GitHub庫的app/posts/post1.md中找到完整的示例文章。
在我們的例子中,元數據由標題、副標題、類別、發布日期和index.html中卡片對應縮略圖的路徑組成.
我們在HTML文件中使用了元數據,你還記得嗎?元數據規范必須是有效的YAML。示例形式是鍵后面跟著一個冒號和值。最后,冒號后面的值是列表中的第一個也是唯一的元素。這就是我們通過模板中的索引運算符訪問這些值的原因。
假設我們寫完了文章。在我們可以開始轉換之前,還有一件事要做:我們需要為我們的帖子生成縮略圖!為了讓事情更簡單,只需從你的電腦或網絡上隨機選取一張圖片,命名它為placeholder.jpg并把它放到static/images目錄中。GitHub存儲庫中兩篇文章的元數據包含一個代表圖像的鍵值對,值是placeholder.jpg。
注意:在GitHub存儲庫中,您可以找到我提到的兩篇示例文章。
markdown到HTML轉換器
最后,我們可以開始實現markdown to HTML轉換器。因此,我們使用我們在開始時安裝的第三方包Markdown。我們先創建一個新模塊,轉換服務將在其中運行。因此,我們在service目錄中創建了converter.py。我們一步一步看完整個腳本。您可以在GitHub存儲庫中一次查看整個腳本。
首先,我們導入所需的所有內容并創建幾個常量:
ROOT指向我們項目的根。因此,它是包含app的目錄。
POSTS_DIR是以markdown編寫的文章的路徑。
TEMPLATE_DIR分別指向對應的templates目錄。
BLOG_TEMPLATE_文件存儲layout.html的路徑。
INDEX_TEMPLATE_FILE是index.html
BASE_URL是我們項目的默認地址,例如。https://florian-dahlitz.de.默認值(如果不是通過環境變量DOMAIN提供的話)是http://0.0.0.0:5000。
接下來,我們創建一個名為generate_entries的新函數。這是我們定義的唯一一個轉換文章的函數。
在函數中,我們首先獲取POSTS_DIR目錄中所有markdown文件的路徑。pathlib的awesome glob函數幫助我們實現它。
此外,我們定義了Markdown包需要使用的擴展。默認情況下,本文中使用的所有擴展都隨它的安裝一起提供。
注意:您可以在文檔[4]中找到有關擴展的更多信息。
此外,我們實例化了一個新的文件加載程序,并創建了一個在轉換項目時使用的環境。隨后,將創建一個名為all_posts的空列表。此列表將包含我們處理后的所有帖子?,F在,我們進入for循環并遍歷POSTS_DIR中找到的所有文章。
我們啟動for循環,并打印當前正在處理的post的路徑。如果有什么東西出問題了,這尤其有用。然后我們就知道,哪個文章的轉換失敗了。
接下來,我們在默認url之后增加一部分。假設我們有一篇標題為“面向初學者的Python”的文章。我們將文章存儲在一個名為python-for-beginners.md,的文件中,因此生成的url將是http://0.0.0.0:5000/posts/python-for-beginners。
變量url_html存儲的字符串與url相同,只是我們在末尾添加了.html。我們使用此變量定義另一個稱為target_file.的變量。變量指向存儲相應HTML文件的位置。
最后,我們定義了一個變量md,它表示markdown.Markdown的實例,用于將markdown代碼轉換為HTML。您可能會問自己,為什么我們沒有在for循環之前實例化這個實例,而是在內部實例化。當然,對于我們這里的小例子來說,這沒有什么區別(只是執行時間稍微短一點)。但是,如果使用諸如腳注之類的擴展來使用腳注,則需要為每個帖子實例化一個新實例,因為腳注添加后就不會從此實例中刪除。因此,如果您的第一篇文章使用了一些腳注,那么即使您沒有明確定義它們,所有其他文章也將具有相同的腳注。
讓我們轉到for循環中的第一個with代碼塊。
實際上,with代碼塊打開當前post并將其內容讀入變量content。之后調用_md.convert將以markdown方式寫入的內容轉換為HTML。隨后,env環境根據提供的模板BLOG_TEMPLATE_FILE(即layout.html如果你還記得的話)渲染生成的HTML。
第二個with 代碼塊用于將第一個with 代碼塊中創建的文檔寫入目標文件。
以下三行代碼從元數據中獲取發布日期(被發布的日期),將其轉換為正確的格式(RFC 2822),并將其分配回文章的元數據。此外,生成的post_dict被添加到all_posts列表中。
我們現在出了for循環,因此,我們遍歷了posts目錄中找到的所有posts并對其進行了處理。讓我們看看generate_entries函數中剩下的三行代碼。
我們按日期倒序對文章進行排序,所以首先顯示最新的文章。隨后,我們將文章寫到模板目錄一個新創建的index.html文件中。別把index.html錯認為templates/shared目錄中的那個。templates/shared目錄中的是模板,這個是我們要使用Flask服務的生成的。
最后我們在函數generate_entries之后添加以下if語句。
這意味著如果我們通過命令行執行文件,它將調用generate_entries函數。
太棒了,我們完成了converter.py腳本!讓我們從項目的根目錄運行以下命令來嘗試:
您應該看到一些正在轉換的文件的路徑。假設您編寫了兩篇文章或使用了GitHub存儲庫中的兩篇文章,那么您應該在templates目錄中找到三個新創建的文件。首先是index.html,它直接位于templates目錄中,其次是templates/posts目錄中的兩個HTML文件,它們對應于markdown文件。
最后啟動Flask應用程序并轉到http://0.0.0.0:5000。
總結
太棒了,你完成了這個系列的第一部分!在本文中,您已經學習了如何利用Markdown包創建自己的Markdown to HTML生成器。您實現了整個管線,它是高度可擴展的,您將在接下來的文章中看到這一點。
希望你喜歡這篇文章。一定要和你的朋友和同事分享。如果你還沒有,考慮在Twitter上關注我@DahlitzF或者訂閱我的通知,這樣你就不會錯過任何即將發表的文章。保持好奇心,不斷編碼!
參考文獻
Bootstrap (http://getbootstrap.com/)
Primer on Jinja Templating (https://realpython.com/primer-on-jinja-templating/)
Bootstrap Card (https://getbootstrap.com/docs/4.4/components/card/)
Python-Markdown Extensions (https://python-markdown.github.io/extensions/)
Tweet
英文原文:https://florian-dahlitz.de/blog/build-a-markdown-to-html-conversion-pipeline-using-python
譯者:阿布銩
*請認真填寫需求信息,我們會在24小時內與您取得聯系。