Python使用標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)將Word文檔轉(zhuǎn)換為HTML
在日常文檔處理中,我們經(jīng)常需要將Word文檔轉(zhuǎn)換為HTML格式以便在網(wǎng)頁(yè)上展示。雖然存在多種第三方庫(kù)可以實(shí)現(xiàn)這一功能,但Python的標(biāo)準(zhǔn)庫(kù)同樣提供了強(qiáng)大而靈活的工具來(lái)處理.docx文件。本文將詳細(xì)解析一個(gè)使用純Python標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)的Word到HTML轉(zhuǎn)換腳本,展示如何直接處理.docx文件的內(nèi)部結(jié)構(gòu)。
1. Word文檔格式基礎(chǔ):了解.docx文件本質(zhì)
.docx文件本質(zhì)上是一個(gè)ZIP壓縮包,其中包含多個(gè)XML文件和其他資源。這種格式基于Office Open XML標(biāo)準(zhǔn),使得我們可以通過(guò)解壓和解析XML來(lái)直接訪問(wèn)文檔內(nèi)容。與使用第三方庫(kù)如python-docx相比,直接使用標(biāo)準(zhǔn)庫(kù)處理.docx文件提供了更底層的控制能力,能夠?qū)崿F(xiàn)更精細(xì)的轉(zhuǎn)換控制。
腳本首先利用zipfile模塊解壓.docx文件,然后通過(guò)xml.etree.ElementTree解析XML結(jié)構(gòu)。這種方法不依賴任何外部依賴,保證了代碼的輕量性和可移植性。
2. 腳本核心功能解析
2.1 文檔結(jié)構(gòu)分析
腳本通過(guò)list_docx_contents函數(shù)列出.docx文件中的所有內(nèi)部文件,這些文件包括文檔主體、樣式、關(guān)系、媒體資源等。這種分析有助于理解Word文檔的復(fù)雜結(jié)構(gòu),為準(zhǔn)確提取內(nèi)容奠定基礎(chǔ)。
def list_docx_contents(filepath):
"""列出 Word 文檔中的所有文件"""
with zipfile.ZipFile(filepath, 'r') as docx:
return docx.namelist()
2.2 文本內(nèi)容提取
通過(guò)extract_text_from_docx函數(shù),腳本從word/document.xml中提取所有文本內(nèi)容。Word文檔使用特定的XML命名空間和結(jié)構(gòu)來(lái)存儲(chǔ)文本,腳本需要正確解析這些結(jié)構(gòu)才能準(zhǔn)確提取內(nèi)容。
2.3 文檔結(jié)構(gòu)解析
analyze_docx_structure函數(shù)提供了深入的文檔結(jié)構(gòu)分析,包括段落樣式、列表屬性、文本格式等。這一功能對(duì)于理解Word文檔的視覺(jué)層次結(jié)構(gòu)至關(guān)重要,是實(shí)現(xiàn)高質(zhì)量HTML轉(zhuǎn)換的基礎(chǔ)。
3. 轉(zhuǎn)換過(guò)程關(guān)鍵技術(shù)細(xì)節(jié)
3.1 段落處理與格式轉(zhuǎn)換
腳本的process_paragraph函數(shù)負(fù)責(zé)將Word段落轉(zhuǎn)換為相應(yīng)的HTML元素。它識(shí)別各種段落樣式(如標(biāo)題、正文、列表項(xiàng))并應(yīng)用對(duì)應(yīng)的HTML標(biāo)簽:
- 標(biāo)題段落(Heading1-Heading6)轉(zhuǎn)換為
<h1>-<h6>標(biāo)簽 - 普通段落轉(zhuǎn)換為
<p>標(biāo)簽 - 列表項(xiàng)轉(zhuǎn)換為
<li>標(biāo)簽,并自動(dòng)管理<ul>或<ol>父容器
此外,該函數(shù)還處理段落級(jí)格式設(shè)置,包括對(duì)齊方式、縮進(jìn)、行間距和背景色等。
3.2 文本格式處理
process_paragraph_formatting函數(shù)深入處理段落內(nèi)的豐富文本格式:
- 字體樣式:識(shí)別粗體(
<strong>)、斜體(<em>)、下劃線(<u>)和刪除線 - 上下標(biāo):正確處理上標(biāo)(
<sup>)和下標(biāo)(<sub>) - 字體屬性:提取字體大小、顏色、高亮和字符間距
- 特殊內(nèi)容:處理超鏈接、圖片、表單字段和修訂標(biāo)記(刪除和插入內(nèi)容)
這種精細(xì)的格式處理確保了轉(zhuǎn)換后的HTML能夠保留原文檔的視覺(jué)特征。
3.3 表格轉(zhuǎn)換
process_table函數(shù)處理Word表格的復(fù)雜結(jié)構(gòu),支持:
- 基本表格結(jié)構(gòu)(
<table>、<tr>、<td>) - 合并單元格(跨行rowspan和跨列colspan)
- 表頭識(shí)別(第一行自動(dòng)使用
<th>標(biāo)簽)
該函數(shù)使用矩陣跟蹤法正確處理單元格合并關(guān)系,確保復(fù)雜表格結(jié)構(gòu)的準(zhǔn)確轉(zhuǎn)換。
3.4 圖片和超鏈接處理
腳本能夠提取文檔中的圖片并將其轉(zhuǎn)換為Base64編碼的HTML圖片標(biāo)簽,同時(shí)正確處理內(nèi)部和外部超鏈接:
def process_image(docx, image_id, doc_rels):
"""處理圖片"""
# 查找圖片關(guān)系并轉(zhuǎn)換為Base64編碼
# ...
def process_hyperlink(rel_id, text, doc_rels):
"""處理超鏈接"""
# 查找關(guān)系中的目標(biāo)URL并生成<a>標(biāo)簽
# ...
這種方法確保圖片和鏈接在HTML中正確顯示,即使圖片是嵌入在.docx文件中的。
4. 高級(jí)功能實(shí)現(xiàn)
4.1 頁(yè)眉頁(yè)腳處理
process_headers_footers函數(shù)單獨(dú)處理文檔的頁(yè)眉和頁(yè)腳內(nèi)容,將其轉(zhuǎn)換為具有特定樣式的HTML元素,保持文檔的完整結(jié)構(gòu)。
4.2 修訂標(biāo)記處理
腳本能夠識(shí)別和處理Word的修訂標(biāo)記(跟蹤更改),將刪除的內(nèi)容顯示為帶刪除線的文本,插入的內(nèi)容顯示為帶背景高亮,這在協(xié)作編輯環(huán)境中特別有用。
4.3 樣式應(yīng)用
生成的HTML包含完整的CSS樣式定義,確保轉(zhuǎn)換結(jié)果在瀏覽器中的顯示效果接近原Word文檔。腳本允許自定義樣式表,滿足不同的視覺(jué)需求。
5. 使用方法和輸出示例
運(yùn)行腳本非常簡(jiǎn)單,只需執(zhí)行主函數(shù)即可完成整個(gè)轉(zhuǎn)換過(guò)程:
def main():
print("Word 文檔處理工具")
print_docx_info('test_input.docx') # 分析文檔結(jié)構(gòu)
html_content = docx_to_html('test_input.docx') # 轉(zhuǎn)換為HTML
save_html('output.html', html_content) # 保存結(jié)果
腳本會(huì)輸出文檔結(jié)構(gòu)分析、轉(zhuǎn)換進(jìn)度和結(jié)果文件路徑,提供完整的處理反饋。
6. 與其他方法的比較
與使用第三方庫(kù)(如python-docx、Apache POI等)相比,這種基于標(biāo)準(zhǔn)庫(kù)的方法具有獨(dú)特優(yōu)勢(shì):
- 零依賴:僅使用Python標(biāo)準(zhǔn)庫(kù),無(wú)需安裝額外包
- 深度控制:直接訪問(wèn).docx文件內(nèi)部結(jié)構(gòu),可實(shí)現(xiàn)高度定制化轉(zhuǎn)換
- 學(xué)習(xí)價(jià)值:深入了解Word文檔的底層結(jié)構(gòu)和Open XML標(biāo)準(zhǔn)
然而,這種方法也需要更多的代碼量和對(duì)XML結(jié)構(gòu)的深入理解,在開(kāi)發(fā)效率上可能不如使用成熟的第三方庫(kù)。
7. 實(shí)際應(yīng)用場(chǎng)景
這種Word到HTML轉(zhuǎn)換技術(shù)在多個(gè)領(lǐng)域有廣泛應(yīng)用:
- 內(nèi)容管理系統(tǒng):將Word格式的內(nèi)容轉(zhuǎn)換為網(wǎng)頁(yè)展示
- 文檔自動(dòng)化:批量處理大量Word文檔并發(fā)布為網(wǎng)頁(yè)
- 協(xié)作平臺(tái):保留修訂記錄和評(píng)論的文檔轉(zhuǎn)換
- 電子郵件模板:將Word設(shè)計(jì)的模板轉(zhuǎn)換為HTML郵件格式
8. 總結(jié)與擴(kuò)展建議
本文詳細(xì)解析了使用Python標(biāo)準(zhǔn)庫(kù)將Word文檔轉(zhuǎn)換為HTML的完整實(shí)現(xiàn)。這種方法不依賴任何第三方庫(kù),通過(guò)直接解析.docx文件的ZIP和XML結(jié)構(gòu),實(shí)現(xiàn)了高質(zhì)量的文檔轉(zhuǎn)換。
腳本的主要優(yōu)點(diǎn)包括完整的格式支持、精細(xì)的轉(zhuǎn)換控制和零外部依賴。如果需要進(jìn)一步擴(kuò)展功能,可以考慮添加對(duì)更復(fù)雜元素(如圖表、公式、注釋)的支持,或優(yōu)化輸出HTML的樣式和響應(yīng)式設(shè)計(jì)。
通過(guò)深入理解這一實(shí)現(xiàn),開(kāi)發(fā)者可以更好地掌握Word文檔處理的底層原理,并根據(jù)具體需求定制更專業(yè)的文檔轉(zhuǎn)換解決方案。
9.完整代碼
# Word 文檔處理腳本
# 使用 Python 標(biāo)準(zhǔn)庫(kù)處理 .docx 文件并轉(zhuǎn)換為 HTML
import zipfile
import xml.etree.ElementTree as ET
import html
import base64
import os
def list_docx_contents(filepath):
"""列出 Word 文檔中的所有文件"""
with zipfile.ZipFile(filepath, 'r') as docx:
return docx.namelist()
def extract_text_from_docx(filepath):
"""從 Word 文檔中提取文本內(nèi)容"""
with zipfile.ZipFile(filepath) as docx:
# 讀取 document.xml
xml_content = docx.read('word/document.xml')
tree = ET.fromstring(xml_content)
# 定義命名空間
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
}
# 提取所有段落文本
paragraphs = tree.findall('.//w:p', namespaces)
text_parts = []
for para in paragraphs:
# 提取段落中所有文本
texts = para.findall('.//w:t', namespaces)
para_text = ''.join([t.text for t in texts if t.text])
if para_text:
text_parts.append(para_text)
return '\n'.join(text_parts)
def analyze_docx_structure(filepath):
"""分析 Word 文檔結(jié)構(gòu)"""
with zipfile.ZipFile(filepath) as docx:
# 讀取 document.xml
xml_content = docx.read('word/document.xml')
tree = ET.fromstring(xml_content)
# 定義命名空間
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
}
# 分析段落
paragraphs = tree.findall('.//w:p', namespaces)
print(f"總共找到 {len(paragraphs)} 個(gè)段落")
for i, para in enumerate(paragraphs[:20]): # 分析前20個(gè)段落
num_pr = para.find('.//w:numPr', namespaces)
p_style_elem = para.find('.//w:pStyle', namespaces)
p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None
# 檢查段落中是否有加粗文本
bold_runs = []
for run in para.findall('.//w:r', namespaces):
text_elem = run.find('.//w:t', namespaces)
bold_elem = run.find('.//w:b', namespaces)
if text_elem is not None and text_elem.text:
bold_runs.append((text_elem.text, bold_elem is not None))
# 提取文本
texts = [r.find('.//w:t', namespaces) for r in para.findall('.//w:r', namespaces)]
para_text = ''.join([t.text for t in texts if t is not None and t.text])
# 檢查是否有加粗文本
has_bold = any(bold for _, bold in bold_runs)
# 顯示哪些具體文本是加粗的
bold_texts = [text for text, is_bold in bold_runs if is_bold]
print(f"段落 {i}: 樣式={p_style}, 列表屬性={num_pr is not None}, 有加粗={has_bold}, 加粗文本={bold_texts}, 文本='{para_text[:50]}{'...' if len(para_text) > 50 else ''}'")
def process_hyperlink(rel_id, text, doc_rels):
"""處理超鏈接"""
# 查找關(guān)系中的目標(biāo)URL
target_url = None
for rel in doc_rels.findall('.//Relationship'):
if rel.get('Id') == rel_id:
target_url = rel.get('Target')
break
if target_url:
return f'<a href="{html.escape(target_url)}" rel="external nofollow" >{html.escape(text)}</a>'
else:
return html.escape(text)
def process_image(docx, image_id, doc_rels):
"""處理圖片"""
# 查找圖片關(guān)系
image_path = None
for rel in doc_rels.findall('.//Relationship'):
if rel.get('Id') == image_id:
image_path = rel.get('Target')
break
if not image_path:
return '<!-- 圖片未找到 -->'
# 規(guī)范化路徑(處理類似"../media/image1.png"的相對(duì)路徑)
if image_path.startswith('..'):
image_path = image_path[3:] # 移除 "../" 前綴
try:
# 從docx文件中讀取圖片數(shù)據(jù)
image_data = docx.read(f'word/{image_path}')
# 將圖片編碼為base64
encoded_image = base64.b64encode(image_data).decode('utf-8')
# 獲取圖片擴(kuò)展名以確定MIME類型
ext = os.path.splitext(image_path)[1].lower()
mime_type = 'image/png' # 默認(rèn)
if ext == '.jpg' or ext == '.jpeg':
mime_type = 'image/jpeg'
elif ext == '.gif':
mime_type = 'image/gif'
elif ext == '.bmp':
mime_type = 'image/bmp'
elif ext == '.svg':
mime_type = 'image/svg+xml'
return f'<img src="data:{mime_type};base64,{encoded_image}" alt="文檔圖片">'
except Exception as e:
return f'<!-- 圖片加載失敗: {str(e)} -->'
def process_paragraph_formatting(para, namespaces, docx=None, doc_rels=None):
"""處理段落內(nèi)的格式化文本(粗體、斜體、顏色等)"""
html_text = ""
last_text = "" # 用于跟蹤上一個(gè)文本,避免重復(fù)
# 遍歷段落中的所有文本元素
for run in para.findall('.//w:r', namespaces):
# 檢查是否有刪除線文本(修訂模式下的刪除內(nèi)容)
del_text_elem = run.find('.//w:delText', namespaces)
if del_text_elem is not None and del_text_elem.text is not None:
# 處理刪除的文本(帶刪除線)
del_text = del_text_elem.text
escaped_text = html.escape(del_text)
html_text += f'<span style="text-decoration:line-through;">{escaped_text}</span>'
continue
# 檢查是否有插入的文本(修訂模式下的新增內(nèi)容)
ins_elem = run.find('.//w:ins', namespaces)
if ins_elem is not None:
# 獲取插入的文本
ins_texts = ins_elem.findall('.//w:t', namespaces)
for ins_text_elem in ins_texts:
if ins_text_elem.text:
ins_text = ins_text_elem.text
escaped_text = html.escape(ins_text)
html_text += f'<span style="background-color:#ccffcc;">{escaped_text}</span>'
continue
# 提取文本
text_elem = run.find('.//w:t', namespaces)
if text_elem is None or text_elem.text is None:
# 檢查是否是制表符
if run.find('.//w:tab', namespaces) is not None:
html_text += " " # 處理制表符
continue
# 檢查是否是圖片
drawing_elem = run.find('.//w:drawing', namespaces)
if drawing_elem is not None and docx is not None and doc_rels is not None:
# 處理圖片
blip_elem = drawing_elem.find('.//a:blip', {
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'
})
if blip_elem is not None:
embed_id = blip_elem.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
if embed_id:
img_html = process_image(docx, embed_id, doc_rels)
html_text += img_html
continue
# 檢查是否是下劃線字符(空的文本但有下劃線格式)
underline_elem = run.find('.//w:u', namespaces)
if underline_elem is not None:
# 獲取下劃線類型
underline_val = underline_elem.get(f'{{{namespaces["w"]}}}val')
# 添加表單字段占位符
html_text += '<span class="form-field"> </span>'
continue
text = text_elem.text
# 檢查是否是重復(fù)文本(有時(shí)候Word中會(huì)有重復(fù)的文本節(jié)點(diǎn))
if text == last_text:
continue
last_text = text
escaped_text = html.escape(text)
# 檢查超鏈接
hyperlink_elem = run.find('.//w:hyperlink', namespaces)
if hyperlink_elem is not None and doc_rels is not None:
rel_id = hyperlink_elem.get(f'{{{namespaces["w"]}}}anchor') or hyperlink_elem.get(f'{{{namespaces["w"]}}}id')
if rel_id:
hyperlink_html = process_hyperlink(rel_id, text, doc_rels)
html_text += hyperlink_html
continue
# 檢查格式
bold_elem = run.find('.//w:b', namespaces)
bold = False
if bold_elem is not None:
# 檢查是否明確設(shè)置了val屬性為false
val = bold_elem.get(f'{{{namespaces["w"]}}}val')
if val is None or val != 'false':
bold = True
# 檢查字體,如果使用黑體等加粗字體,也應(yīng)視為加粗
rpr_elem = run.find('.//w:rPr', namespaces)
if rpr_elem is not None:
# 檢查字體設(shè)置
font_elem = rpr_elem.find('.//w:rFonts', namespaces)
if font_elem is not None:
# 獲取各種字體屬性
ascii_font = font_elem.get(f'{{{namespaces["w"]}}}ascii')
hansi_font = font_elem.get(f'{{{namespaces["w"]}}}hAnsi')
east_asia_font = font_elem.get(f'{{{namespaces["w"]}}}eastAsia')
# 如果使用黑體等加粗字體,視為加粗
bold_fonts = ['黑體', 'Bold', 'Arial Black', 'Impact']
if (ascii_font and any(bf in ascii_font for bf in bold_fonts)) or \
(hansi_font and any(bf in hansi_font for bf in bold_fonts)) or \
(east_asia_font and any(bf in east_asia_font for bf in bold_fonts)):
bold = True
italic_elem = run.find('.//w:i', namespaces)
italic = False
if italic_elem is not None:
val = italic_elem.get(f'{{{namespaces["w"]}}}val')
if val is None or val != 'false':
italic = True
underline_elem = run.find('.//w:u', namespaces)
underline = False
if underline_elem is not None:
val = underline_elem.get(f'{{{namespaces["w"]}}}val')
if val is None or val != 'none':
underline = True
strike_elem = run.find('.//w:strike', namespaces)
strike = False
if strike_elem is not None:
val = strike_elem.get(f'{{{namespaces["w"]}}}val')
if val is None or val != 'false':
strike = True
# 檢查上下標(biāo)
vert_align_elem = run.find('.//w:vertAlign', namespaces)
vert_align = None
if vert_align_elem is not None:
align_val = vert_align_elem.get(f'{{{namespaces["w"]}}}val')
if align_val == 'superscript':
vert_align = 'sup'
elif align_val == 'subscript':
vert_align = 'sub'
# 檢查字體大小
sz_elem = run.find('.//w:sz', namespaces)
font_size = None
if sz_elem is not None:
sz_val = sz_elem.get(f'{{{namespaces["w"]}}}val')
if sz_val:
# Word中的字號(hào)是半點(diǎn)為單位,需要轉(zhuǎn)換為pt
font_size = f"{int(sz_val)/2}pt"
# 檢查高亮色
highlight_elem = run.find('.//w:highlight', namespaces)
background_color = None
if highlight_elem is not None:
highlight_val = highlight_elem.get(f'{{{namespaces["w"]}}}val')
# 需要將Word顏色名稱映射為CSS顏色值
color_map = {
'yellow': '#ffff00',
'green': '#00ff00',
'red': '#ff0000',
'blue': '#0000ff',
'cyan': '#00ffff',
'magenta': '#ff00ff',
'black': '#000000',
'white': '#ffffff'
}
background_color = color_map.get(highlight_val, highlight_val)
# 檢查顏色
color_elem = run.find('.//w:color', namespaces)
color = None
if color_elem is not None:
color_val = color_elem.get(f'{{{namespaces["w"]}}}val')
if color_val and color_val != '000000': # 忽略黑色(默認(rèn)顏色)
# 處理自動(dòng)顏色
if color_val.lower() == 'auto':
color = 'inherit'
else:
color = f"#{color_val}"
# 檢查字符間距
spacing_elem = run.find('.//w:spacing', namespaces)
letter_spacing = None
if spacing_elem is not None:
spacing_val = spacing_elem.get(f'{{{namespaces["w"]}}}val')
if spacing_val:
# Word中字符間距單位是 twentieths of a point (1/1440 inch)
# 轉(zhuǎn)換為CSS的em單位
spacing_em = int(spacing_val) / 120 # 120 = 20 * 6 (approximation)
if spacing_em != 0:
letter_spacing = f"{spacing_em:.2f}em"
# 檢查背景色
shd_elem = rpr_elem.find('.//w:shd', namespaces) if rpr_elem is not None else None
shading_color = None
if shd_elem is not None:
shd_fill = shd_elem.get(f'{{{namespaces["w"]}}}fill')
if shd_fill and shd_fill != 'auto' and shd_fill != 'clear':
shading_color = f"#{shd_fill}"
# 構(gòu)建樣式字符串
styles = []
if font_size:
styles.append(f"font-size:{font_size}")
if background_color:
styles.append(f"background-color:{background_color}")
if color and color != 'inherit':
styles.append(f"color:{color}")
elif color == 'inherit':
styles.append(f"color:{color}")
if letter_spacing:
styles.append(f"letter-spacing:{letter_spacing}")
if shading_color:
styles.append(f"background-color:{shading_color}")
style_attr = f' style="{";".join(styles)}"' if styles else ''
# 應(yīng)用格式
if bold:
escaped_text = f'<strong>{escaped_text}</strong>'
if italic:
escaped_text = f'<em>{escaped_text}</em>'
if underline:
escaped_text = f'<u>{escaped_text}</u>' # 使用標(biāo)準(zhǔn)HTML下劃線標(biāo)簽
if strike:
escaped_text = f'<span style="text-decoration:line-through">{escaped_text}</span>'
if vert_align:
escaped_text = f'<{vert_align}>{escaped_text}</{vert_align}>'
if style_attr:
escaped_text = f'<span{style_attr}>{escaped_text}</span>'
html_text += escaped_text
return html_text
def process_paragraph(p, namespaces, docx=None, doc_rels=None):
"""處理段落"""
# 獲取段落屬性
p_pr = p.find('./w:pPr', namespaces)
# 檢查段落樣式
p_style_elem = p_pr.find('.//w:pStyle', namespaces) if p_pr is not None else None
p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None
# 如果是標(biāo)題樣式,直接返回標(biāo)題標(biāo)簽
if p_style and p_style.startswith('Heading'):
# 處理段落中的文本
formatted_text = process_paragraph_formatting(p, namespaces, docx, doc_rels)
# 根據(jù)樣式確定標(biāo)題級(jí)別
heading_level = 1
if p_style == 'Heading1':
heading_level = 1
elif p_style == 'Heading2':
heading_level = 2
elif p_style == 'Heading3':
heading_level = 3
elif p_style == 'Heading4':
heading_level = 4
elif p_style == 'Heading5':
heading_level = 5
elif p_style == 'Heading6':
heading_level = 6
return f'<h{heading_level}>{formatted_text}</h{heading_level}>'
# 處理段落樣式
style_attrs = []
# 處理文本對(duì)齊
jc_elem = p_pr.find('.//w:jc', namespaces) if p_pr is not None else None
if jc_elem is not None:
jc_val = jc_elem.get(f'{{{namespaces["w"]}}}val')
if jc_val == 'center':
style_attrs.append('text-align:center')
elif jc_val == 'right':
style_attrs.append('text-align:right')
elif jc_val == 'both':
style_attrs.append('text-align:justify')
else:
style_attrs.append('text-align:left')
else:
style_attrs.append('text-align:left')
# 處理段落縮進(jìn)
ind_elem = p_pr.find('.//w:ind', namespaces) if p_pr is not None else None
if ind_elem is not None:
# 左縮進(jìn)
left_ind = ind_elem.get(f'{{{namespaces["w"]}}}left')
if left_ind:
# Word中單位為twips,1 twip = 1/1440 inch,約等于1/20 pt
style_attrs.append(f'margin-left:{int(left_ind)/20}pt')
# 首行縮進(jìn)
first_line_ind = ind_elem.get(f'{{{namespaces["w"]}}}firstLine')
if first_line_ind:
style_attrs.append(f'text-indent:{int(first_line_ind)/20}pt')
# 懸掛縮進(jìn)
hanging_ind = ind_elem.get(f'{{{namespaces["w"]}}}hanging')
if hanging_ind:
# 懸掛縮進(jìn)需要特殊處理
style_attrs.append(f'text-indent:-{int(hanging_ind)/20}pt;padding-left:{int(hanging_ind)/20}pt')
# 處理行間距
spacing_elem = p_pr.find('.//w:spacing', namespaces) if p_pr is not None else None
if spacing_elem is not None:
# 行間距
line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
if line_val:
# Word中的行距單位是twips (1/20 pt)
# 通常240 twips = 12 pt = 單倍行距
line_height = int(line_val) / 240
if line_height > 0:
# 避免重復(fù)添加line-height樣式
existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
if not existing_line_height:
style_attrs.append(f'line-height:{line_height}em')
# 行距規(guī)則
line_rule = spacing_elem.get(f'{{{namespaces["w"]}}}lineRule')
if line_rule == 'auto':
# 自動(dòng)行距已在上面處理
pass
elif line_rule == 'atLeast':
# 最小行距
line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
if line_val:
min_line_height = int(line_val) / 240
# 避免重復(fù)添加line-height樣式
existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
if not existing_line_height:
style_attrs.append(f'line-height:min({min_line_height}em)')
elif line_rule == 'exact':
# 固定行距
line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
if line_val:
fixed_line_height = int(line_val) / 240
# 避免重復(fù)添加line-height樣式
existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
if not existing_line_height:
style_attrs.append(f'line-height:{fixed_line_height}em')
# 處理段落底紋/背景色
shd_elem = p_pr.find('.//w:shd', namespaces) if p_pr is not None else None
if shd_elem is not None:
shd_fill = shd_elem.get(f'{{{namespaces["w"]}}}fill')
if shd_fill and shd_fill != 'auto' and shd_fill != 'clear':
style_attrs.append(f'background-color:#{shd_fill}')
style_str = f' style="{";".join(style_attrs)}"' if style_attrs else ''
# 處理段落中的文本
formatted_text = process_paragraph_formatting(p, namespaces, docx, doc_rels)
# 檢查是否為列表項(xiàng)
num_pr = p_pr.find('.//w:numPr', namespaces) if p_pr is not None else None
if num_pr is not None:
# 獲取編號(hào)信息以確定是有序還是無(wú)序列表
num_id_elem = num_pr.find('.//w:numId', namespaces)
ilvl_elem = num_pr.find('.//w:ilvl', namespaces)
# 默認(rèn)使用無(wú)序列表
list_type = 'ul'
# 如果有編號(hào)ID,則嘗試判斷是否為有序列表
if num_id_elem is not None:
num_id_val = num_id_elem.get(f'{{{namespaces["w"]}}}val')
# 可以根據(jù)num_id_val的值來(lái)判斷是否為有序列表
# 這里簡(jiǎn)化處理,默認(rèn)認(rèn)為所有列表都是無(wú)序列表
# 實(shí)際項(xiàng)目中可以從word/numbering.xml中獲取詳細(xì)信息
return f'<li{style_str}>{formatted_text}</li>'
else:
return f'<p{style_str}>{formatted_text}</p>'
def process_table(table, namespaces, docx=None, doc_rels=None):
"""處理表格,支持合并單元格等復(fù)雜結(jié)構(gòu)"""
html_parts = ['<table>']
# 處理表格行
rows = table.findall('.//w:tr', namespaces)
# 創(chuàng)建一個(gè)矩陣來(lái)跟蹤已被占用的單元格位置,用于處理跨行合并
cell_matrix = [[False for _ in range(20)] for _ in range(len(rows))] # 假設(shè)最多20列
for i, row in enumerate(rows):
html_parts.append('<tr>')
# 處理單元格
cells = row.findall('.//w:tc', namespaces)
cell_index = 0 # 當(dāng)前行的實(shí)際單元格索引
matrix_col = 0 # 在矩陣中的列位置
# 找到下一個(gè)可用的列位置
while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
matrix_col += 1
for cell in cells:
# 跳過(guò)已經(jīng)被前面的跨行單元格占據(jù)的位置
while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
matrix_col += 1
# 確定是表頭還是普通單元格
cell_tag = 'th' if i == 0 else 'td'
# 檢查單元格合并屬性
tc_pr = cell.find('.//w:tcPr', namespaces)
colspan = ''
rowspan = ''
if tc_pr is not None:
# 檢查水平合并 (gridSpan)
grid_span = tc_pr.find('.//w:gridSpan', namespaces)
if grid_span is not None:
span_val = grid_span.get(f'{{{namespaces["w"]}}}val')
if span_val:
colspan = f' colspan="{span_val}"'
# 標(biāo)記這些列位置已被占用
span_count = int(span_val)
for col_offset in range(span_count):
if matrix_col + col_offset < len(cell_matrix[i]):
cell_matrix[i][matrix_col + col_offset] = True
# 檢查垂直合并 (vMerge)
v_merge = tc_pr.find('.//w:vMerge', namespaces)
if v_merge is not None:
merge_val = v_merge.get(f'{{{namespaces["w"]}}}val')
# 只有當(dāng)是合并起點(diǎn)時(shí)才添加 rowspan
if merge_val == 'restart':
# 查找跨越的行數(shù)
rowspan_count = 1
# 計(jì)算需要合并多少行
for next_row_idx in range(i + 1, len(rows)):
next_row = rows[next_row_idx]
next_cells = next_row.findall('.//w:tc', namespaces)
# 檢查下一行對(duì)應(yīng)位置是否有繼續(xù)合并的標(biāo)記
# 先找到下一行中對(duì)應(yīng)位置的單元格
next_cell_found = False
next_cell_index = 0
current_col_pos = 0
# 計(jì)算在下一行中對(duì)應(yīng)列位置的單元格
for next_cell in next_cells:
next_tc_pr = next_cell.find('.//w:tcPr', namespaces)
if next_tc_pr is not None:
next_grid_span = next_tc_pr.find('.//w:gridSpan', namespaces)
next_colspan = 1
if next_grid_span is not None:
next_span_val = next_grid_span.get(f'{{{namespaces["w"]}}}val')
if next_span_val:
next_colspan = int(next_span_val)
# 檢查當(dāng)前位置是否是我們尋找的位置
if current_col_pos == matrix_col:
# 檢查這個(gè)單元格是否是繼續(xù)合并的單元格
next_v_merge = next_tc_pr.find('.//w:vMerge', namespaces)
if next_v_merge is not None:
next_merge_val = next_v_merge.get(f'{{{namespaces["w"]}}}val')
if next_merge_val is None: # 繼續(xù)合并
rowspan_count += 1
# 標(biāo)記這個(gè)位置被跨行單元格占據(jù)
for col_offset in range(next_colspan):
if matrix_col + col_offset < len(cell_matrix[next_row_idx]):
cell_matrix[next_row_idx][matrix_col + col_offset] = True
next_cell_found = True
break
else:
next_cell_found = True
break
else:
# 沒(méi)有vMerge標(biāo)記,說(shuō)明合并結(jié)束
next_cell_found = True
break
current_col_pos += next_colspan
if not next_cell_found:
# 如果沒(méi)找到對(duì)應(yīng)的單元格,可能是被跨列占據(jù)了位置
# 檢查這個(gè)位置是否被標(biāo)記為已占用
if matrix_col < len(cell_matrix[next_row_idx]) and not cell_matrix[next_row_idx][matrix_col]:
# 位置未被占用,說(shuō)明合并結(jié)束
break
else:
# 位置被占用,繼續(xù)檢查下一行
rowspan_count += 1
# 標(biāo)記這個(gè)位置被跨行單元格占據(jù)
if matrix_col < len(cell_matrix[next_row_idx]):
cell_matrix[next_row_idx][matrix_col] = True
elif next_cell_found:
next_v_merge = None
if next_cell_index < len(next_cells):
next_tc_pr = next_cells[next_cell_index].find('.//w:tcPr', namespaces)
if next_tc_pr is not None:
next_v_merge = next_tc_pr.find('.//w:vMerge', namespaces)
if next_v_merge is not None:
next_merge_val = next_v_merge.get(f'{{{namespaces["w"]}}}val')
if next_merge_val is None: # 繼續(xù)合并
rowspan_count += 1
# 標(biāo)記這個(gè)位置被跨行單元格占據(jù)
if matrix_col < len(cell_matrix[next_row_idx]):
cell_matrix[next_row_idx][matrix_col] = True
else:
break
else:
break
if rowspan_count > 1:
rowspan = f' rowspan="{rowspan_count}"'
else:
# 不是合并起點(diǎn),跳過(guò)這個(gè)單元格
cell_index += 1
continue
else:
# 標(biāo)記當(dāng)前位置被使用
if matrix_col < len(cell_matrix[i]):
cell_matrix[i][matrix_col] = True
# 如果沒(méi)有設(shè)置rowspan,也要標(biāo)記當(dāng)前位置被使用
if not rowspan and matrix_col < len(cell_matrix[i]):
cell_matrix[i][matrix_col] = True
# 提取單元格中的段落
cell_paragraphs = cell.findall('.//w:p', namespaces)
cell_content = ""
for para in cell_paragraphs:
para_content = process_paragraph_formatting(para, namespaces, docx, doc_rels)
if para_content:
cell_content += para_content + "<br>" if cell_content else para_content
# 如果沒(méi)有內(nèi)容,添加空白字符以確保單元格可見(jiàn)
if not cell_content:
cell_content = " "
html_parts.append(f'<{cell_tag}{colspan}{rowspan}>{cell_content}</{cell_tag}>')
cell_index += 1
matrix_col += 1
# 調(diào)整matrix_col到下一個(gè)可用位置
while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
matrix_col += 1
html_parts.append('</tr>')
html_parts.append('</table>')
return '\n'.join(html_parts)
def process_textbox(textBox, namespaces, docx=None, doc_rels=None):
"""處理文本框"""
# 文本框本質(zhì)上是一個(gè)容器,包含段落或其他元素
html_parts = ['<div class="textbox">']
# 處理文本框中的所有段落
paragraphs = textBox.findall('.//w:p', namespaces)
for p in paragraphs:
html_parts.append(process_paragraph(p, namespaces, docx, doc_rels))
html_parts.append('</div>')
return '\n'.join(html_parts)
def process_headers_footers(docx, namespaces, doc_rels):
"""處理頁(yè)眉和頁(yè)腳"""
header_content = []
footer_content = []
# 嘗試讀取頁(yè)眉
try:
header_files = [f for f in docx.namelist() if f.startswith('word/header') and f.endswith('.xml')]
for header_file in header_files:
header_xml = docx.read(header_file)
header_tree = ET.fromstring(header_xml)
# 處理頁(yè)眉中的段落
paragraphs = header_tree.findall('.//w:p', namespaces)
for p in paragraphs:
header_text = process_paragraph(p, namespaces, docx, doc_rels)
if header_text.strip():
header_content.append(header_text)
except:
pass # 頁(yè)眉處理失敗時(shí)忽略
# 嘗試讀取頁(yè)腳
try:
footer_files = [f for f in docx.namelist() if f.startswith('word/footer') and f.endswith('.xml')]
for footer_file in footer_files:
footer_xml = docx.read(footer_file)
footer_tree = ET.fromstring(footer_xml)
# 處理頁(yè)腳中的段落
paragraphs = footer_tree.findall('.//w:p', namespaces)
for p in paragraphs:
footer_text = process_paragraph(p, namespaces, docx, doc_rels)
if footer_text.strip():
footer_content.append(footer_text)
except:
pass # 頁(yè)腳處理失敗時(shí)忽略
return header_content, footer_content
def process_revisions(tree, namespaces):
"""處理修訂(刪除和插入的內(nèi)容)"""
revision_content = []
# 處理刪除的內(nèi)容
del_elements = tree.findall('.//w:del', namespaces)
for del_elem in del_elements:
del_texts = del_elem.findall('.//w:delText', namespaces)
for del_text in del_texts:
if del_text.text:
revision_content.append(f'<span style="text-decoration:line-through;background-color:#ffcccc;">{html.escape(del_text.text)}</span>')
# 處理插入的內(nèi)容
ins_elements = tree.findall('.//w:ins', namespaces)
for ins_elem in ins_elements:
ins_texts = ins_elem.findall('.//w:t', namespaces)
for ins_text in ins_texts:
if ins_text.text:
revision_content.append(f'<span style="background-color:#ccffcc;">{html.escape(ins_text.text)}</span>')
return revision_content
def docx_to_html(filepath):
"""將 Word 文檔轉(zhuǎn)換為 HTML"""
with zipfile.ZipFile(filepath) as docx:
# 讀取 document.xml
xml_content = docx.read('word/document.xml')
tree = ET.fromstring(xml_content)
# 讀取關(guān)系文件
try:
rels_content = docx.read('word/_rels/document.xml.rels')
doc_rels = ET.fromstring(rels_content)
except:
doc_rels = None
# 定義命名空間
namespaces = {
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
}
# 創(chuàng)建 HTML 結(jié)構(gòu)
html_output = ['<!DOCTYPE html>', '<html>', '<head>', '<meta charset="UTF-8">',
'<title>Word 文檔轉(zhuǎn)換</title>',
'<style>',
'body { font-family: sans-serif; margin: 40px auto; max-width: 800px; line-height: 1.6; }',
'h1 { color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; }',
'h2 { color: #666; border-bottom: 1px solid #eee; padding-bottom: 5px; }',
'h3 { color: #999; }',
'h4 { color: #333; }',
'h5 { color: #666; }',
'h6 { color: #999; }',
'p { margin: 1em 0; }',
'ul, ol { margin: 1em 0; padding-left: 40px; }',
'li { margin: 0.5em 0; }',
'table { border-collapse: collapse; width: 100%; margin: 1em 0; }',
'td, th { border: 1px solid #ddd; padding: 8px; text-align: left; }',
'strong { font-weight: bold; }',
'em { font-style: italic; }',
'.bold { font-weight: bold; }',
'.italic { font-style: italic; }',
'u { text-decoration: underline; }',
'.underline { text-decoration: underline; }',
'img { max-width: 100%; height: auto; }',
'.header, .footer { background-color: #f5f5f5; padding: 10px; margin: 10px 0; border: 1px solid #ddd; }',
'.header { border-bottom: 2px solid #ccc; }',
'.footer { border-top: 2px solid #ccc; }',
'.textbox { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }',
'.floating-element { position: relative; float: right; margin: 10px 0 10px 10px; }',
'.form-field { border-bottom: 1px solid #000; display: inline-block; min-width: 50px; text-align: center; margin: 0 2px; padding: 0 5px; }',
'</style>',
'</head>', '<body>']
# 處理頁(yè)眉和頁(yè)腳
header_footer_content = process_headers_footers(docx, namespaces, doc_rels)
# 添加頁(yè)眉內(nèi)容到頁(yè)面頂部
header_content = [item for item in header_footer_content if 'class="header"' in item]
footer_content = [item for item in header_footer_content if 'class="footer"' in item]
# 添加頁(yè)眉
html_output.extend(header_content)
# 處理所有元素
# 注意:在Word XML中,內(nèi)容在<w:body>標(biāo)簽內(nèi)
body = tree.find('.//w:body', namespaces)
if body is not None:
# 跟蹤列表狀態(tài)
in_list = False
list_type = None
for elem in body:
if elem.tag.endswith('p'): # 段落
p_pr = elem.find('./w:pPr', namespaces)
p_style_elem = p_pr.find('.//w:pStyle', namespaces) if p_pr is not None else None
p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None
num_pr = p_pr.find('.//w:numPr', namespaces) if p_pr is not None else None
# 檢查是否為標(biāo)題
is_heading = p_style and p_style.startswith('Heading')
# 檢查是否為列表項(xiàng)
is_list_item = num_pr is not None
# 如果之前在列表中,但現(xiàn)在不是列表項(xiàng),則關(guān)閉列表
# 標(biāo)題也不應(yīng)在列表中,所以也要關(guān)閉列表
if in_list and (not is_list_item or is_heading):
html_output.append(f'</{list_type}>')
in_list = False
list_type = None
# 如果之前不在列表中,但現(xiàn)在是列表項(xiàng),則開(kāi)啟列表
if not in_list and is_list_item and not is_heading:
# 嘗試區(qū)分有序和無(wú)序列表
# 簡(jiǎn)單處理:如果有編號(hào)信息則認(rèn)為是有序列表,否則為無(wú)序列表
list_type = 'ol' if num_pr is not None else 'ul'
html_output.append(f'<{list_type}>')
in_list = True
html_output.append(process_paragraph(elem, namespaces, docx, doc_rels))
elif elem.tag.endswith('tbl'): # 表格
# 如果在列表中,則先關(guān)閉列表
if in_list:
html_output.append(f'</{list_type}>')
in_list = False
list_type = None
html_output.append(process_table(elem, namespaces, docx, doc_rels))
# 如果循環(huán)結(jié)束后還在列表中,則關(guān)閉列表
if in_list:
html_output.append(f'</{list_type}>')
else:
# 如果找不到body標(biāo)簽,則直接處理tree的子元素
for elem in tree:
if elem.tag.endswith('p'): # 段落
html_output.append(process_paragraph(elem, namespaces, docx, doc_rels))
elif elem.tag.endswith('tbl'): # 表格
html_output.append(process_table(elem, namespaces, docx, doc_rels))
# 添加頁(yè)腳內(nèi)容到頁(yè)面底部
html_output.extend(footer_content)
html_output.extend(['</body>', '</html>'])
return '\n'.join(html_output)
def print_docx_info(filepath):
"""打印 Word 文檔的基本信息"""
print(f"正在分析文檔: {filepath}")
# 列出文檔內(nèi)容
contents = list_docx_contents(filepath)
print("\n文檔包含以下文件:")
for item in contents[:10]: # 只顯示前10個(gè)
print(f" {item}")
if len(contents) > 10:
print(f" ... 還有 {len(contents) - 10} 個(gè)文件")
# 提取并顯示文本內(nèi)容
print("\n文檔文本內(nèi)容預(yù)覽:")
text = extract_text_from_docx(filepath)
print(text[:500] + "..." if len(text) > 500 else text)
def save_html(filepath, html_content):
"""保存 HTML 到文件"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"HTML 文件已保存到: {filepath}")
def main():
"""主函數(shù)"""
print("Word 文檔處理工具")
print("==================")
try:
# 顯示文檔信息
print_docx_info('test_input.docx')
# 分析文檔結(jié)構(gòu)
print("\n文檔結(jié)構(gòu)分析:")
analyze_docx_structure('test_input.docx')
# 轉(zhuǎn)換為 HTML
print("\n正在將 Word 文檔轉(zhuǎn)換為 HTML...")
html_content = docx_to_html('test_input.docx')
# 保存 HTML 文件
save_html('output.html', html_content)
print("轉(zhuǎn)換完成!")
except FileNotFoundError:
print("未找到 test_input.docx 文件,請(qǐng)確保文件存在于項(xiàng)目根目錄中")
except Exception as e:
print(f"處理文檔時(shí)發(fā)生錯(cuò)誤: {e}")
import traceback
traceback.print_exc()
# 程序入口點(diǎn)
if __name__ == '__main__':
main()
以上就是Python使用標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)將Word文檔轉(zhuǎn)換為HTML的詳細(xì)內(nèi)容,更多關(guān)于Python Word轉(zhuǎn)HTML的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Python實(shí)現(xiàn)將Markdown轉(zhuǎn)為Word、HTML、PDF、PNG和JPG
- 利用Python高效實(shí)現(xiàn)Word轉(zhuǎn)HTML的全流程方案
- Python實(shí)現(xiàn)高效將Word文檔轉(zhuǎn)換為HTML
- Python高效實(shí)現(xiàn)HTML轉(zhuǎn)為Word和PDF
- Python實(shí)現(xiàn)HTML轉(zhuǎn)Word的示例代碼
- 基于Python實(shí)現(xiàn)Word轉(zhuǎn)HTML
- 如何利用Python將html轉(zhuǎn)為pdf、word文件
- python如何實(shí)現(xiàn)word批量轉(zhuǎn)HTML
- Python 實(shí)現(xiàn) Word 轉(zhuǎn) HTML 的三種方法
相關(guān)文章
Python內(nèi)存管理機(jī)制之垃圾回收與引用計(jì)數(shù)操作全過(guò)程
SQLAlchemy是Python中最流行的ORM(對(duì)象關(guān)系映射)框架之一,它提供了高效且靈活的數(shù)據(jù)庫(kù)操作方式,本文將介紹如何使用SQLAlchemy?ORM進(jìn)行數(shù)據(jù)庫(kù)操作,感興趣的朋友跟隨小編一起看看吧2025-09-09
Python使用SymPy和Manim輕松搞定導(dǎo)數(shù)動(dòng)畫
大家好,你有沒(méi)有試過(guò)在 Manim 里做導(dǎo)數(shù)定義的動(dòng)畫,這篇文章小編就來(lái)和大家詳細(xì)介紹一下Python如何使用SymPy和Manim輕松搞定導(dǎo)數(shù)動(dòng)畫吧2026-05-05
Kali Linux安裝ipython2 和 ipython3的方法
今天小編就為大家分享一篇Kali Linux安裝ipython2 和 ipython3的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07
Python實(shí)現(xiàn)修改阿里云DNS域名解析
當(dāng)公網(wǎng)IP是浮動(dòng)的時(shí)候,用一個(gè)域名去實(shí)時(shí)解析,才不會(huì)那么糟糕,本文將介紹如何使用python修改阿里云dns域名解析,感興趣的小伙伴可以了解一下2024-11-11
使用python如何實(shí)現(xiàn)泛型函數(shù)
這篇文章主要介紹了使用python如何實(shí)現(xiàn)泛型函數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
Python腳本實(shí)現(xiàn)DNSPod DNS動(dòng)態(tài)解析域名
這篇文章主要介紹了Python腳本實(shí)現(xiàn)DNSPod DNS動(dòng)態(tài)解析域名,本文直接給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-02-02
Python虛擬機(jī)中描述器的王炸應(yīng)用分享
本篇文章給大家介紹一下描述器在?python?語(yǔ)言當(dāng)中有哪些應(yīng)用,主要介紹如何使用?python?語(yǔ)言實(shí)現(xiàn)?python?內(nèi)置的?proterty?、staticmethod?和?class?method,需要的可以參考一下2023-05-05
關(guān)于jupyter lab安裝及導(dǎo)入tensorflow找不到模塊的問(wèn)題
這篇文章主要介紹了關(guān)于jupyter lab安裝及導(dǎo)入tensorflow找不到模塊的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03

