使用 RAG 实现企业私域知识问答是目前最流行也是最前沿的大模型技术之一，在 高级 RAG 技术学习笔记 这篇笔记中，我介绍了很多种不同的高级 RAG 技术，其关注点更多的是如何检索私域知识以及如何将检索结果灌输到大模型中，并没有深入如何获得这些私域知识。现实中，绝大数企业私域知识都是非结构化的，散落在各种网页、文档或邮件附件里，如果能将这些内容解析出来，对企业来说无疑是巨大的价值。而在这些非结构化的文档中，PDF 文档占比很高，使得 PDF 解析对 RAG 至关重要。PDF 全称 Portable Document Format（可移植文档格式），于 1993 年由 Adobe 公司开发，鉴于其跨平台性、高安全性、开放标准、可搜索性和可访问性等优势，已经成为全球范围内广泛使用的文件格式。Python 中有着大量的 PDF 解析库，这篇笔记对常用的 PDF 解析库做一个盘点，方便自己后期技术选型时做参考。pypdfpypdf 是一个免费且开源的纯 Python PDF 库，能够分割、合并、裁剪和转换 PDF 文件的页面，可以向 PDF 文件添加自定义数据，对 PDF 文件进行加密和解密。当然，pypdf 还可以从 PDF 中提取文本、图片、附件、批注和元数据等。from pypdf import PdfReader reader = PdfReader("./pdfs/example.pdf") number_of_pages = len(reader.pages) print('Total %d pages.' % (number_of_pages)) for i in range(number_of_pages): print('----- Page %d -----' % (i+1)) page = reader.pages[i] text = page.extract_text() print(text) for count, image_file_object in enumerate(page.images): with open(str(count) + image_file_object.name, "wb") as fp: fp.write(image_file_object.data)可以看出 pypdf 的用法较为简单，我们只能拿到每一页的文本和图片内容，拿不到更多的详细信息，比如文本字体和大小，块位置等，这些信息在处理复杂场景时是必不可少的。所以 pypdf 只适合 PDF 的内容比较规整的场景。pdfminer.sixpdfminer.six 也是一个纯 Python 编写的 PDF 库，专注于获取和分析文本数据。pdfminer 最初由 Euske 开发，但是只支持 Python 2，不支持 Python 3，于是社区在他的基础上引入了 six，这是一个无需修改代码，就可以同时兼容 Python 2 和 3 的库，所以叫做 pdfminer.six。使用 extract_text 方法实现类似 pypdf 的效果，直接返回文本：from pdfminer.high_level import extract_text text = extract_text("./pdfs/example.pdf") print(text)或者使用 extract_pages 方法提取元素的详细信息，包括文本的精确位置、字体、大小或颜色：from pdfminer.high_level import extract_pages for page in extract_pages("./pdfs/example.pdf"): for element in page: print(element)这里的元素可能是 LTTextBox、LTFigure、LTLine、LTRect 或 LTImage，它们的层次结构如下所示：其中 LTTextBox 还可以继续遍历得到 LTTextLine，LTTextLine 再继续遍历得到 LTChar：from pdfminer.high_level import extract_pages from pdfminer.layout import LTTextContainer, LTChar for page in extract_pages("./pdfs/example.pdf"): for element in page: if isinstance(element, LTTextContainer): for text_line in element: for character in text_line: if isinstance(character, LTChar): print(character.get_text()) print(character.fontname) print(character.size) else: print(element)布局分析PDF 文件和 .txt 或 Word 在格式上有着很大的不同，它不包含任何类似于段落、句子甚至单词的内容。它由一系列对象及其结构信息组成，这些对象共同描述一个或多个页面的外观，可能还附带有其他交互元素和更高级别的应用程序数据。这使得从 PDF 文件中提取有意义的文本片段变得困难，组成段落的字符与组成表格、页面底部或图表描述的字符没有任何区别。上一节我们知道，通过 pdfminer.six 可以拿到元素的位置信息，通过这些位置信息我们可以重建句子或段落的布局。布局分析由三个不同阶段组成：将字符分组为单词和行，然后将行分组为框，最后以层次结构方式将文本框分组。这是一种最经典的基于规则的 布局分析算法（Layout analysis algorithm）。布局分析依赖于几个重要参数，比如字符间距、行间距、行重叠等，这些参数都是 LAParams 类 的一部分。更多说明请参考 Converting a PDF file to text 这篇文档。pypdfium2PDFium 被认为是开源世界中最高质量的 PDF 渲染引擎之一，它最初是基于福昕软件（Foxit Software）的 PDF SDK 开发的，在 2014 年被 Google 开源。PDFium 支持多种操作系统，包括 Windows、macOS、Linux 等，它还被编译到 iOS、Android 等移动平台上，支持跨平台应用；除了基本的 PDF 渲染功能，PDFium 还支持生成、编辑、文本提取、搜索、注解、表单填充等高级功能。PDFium 是一个高效、可靠的 PDF 渲染引擎，广泛应用于 Chrome 浏览器和其他第三方项目中。其开源性质和丰富的功能使其成为处理 PDF 文档的理想选择，由于基于 C++ 开发，处理大文件速度优于纯 Python 库。pypdfium2 是 PDFium 库的 Python 3 绑定，它提供了一些辅助方法简化 PDFium 库的使用，同时原始的 PDFium/ctypes API 仍然可访问。下面的示例代码演示了如何通过 pypdfium2 的 get_text_bounded() 方法将 PDF 中的文本提取出来：import pypdfium2 as pdfium pdf = pdfium.PdfDocument("./pdfs/example.pdf") for i in range(len(pdf)): print('----- Page %d -----' % (i+1)) page = pdf[i] textpage = page.get_textpage() text_all = textpage.get_text_bounded() print(text_all)和 pdfminer.six 一样，我们也可以通过 get_rect() 获取每个文本块的位置：textpage = page.get_textpage() rect_count = textpage.count_rects() print(rect_count) for i in range(rect_count): rect = textpage.get_rect(i) print(rect) text = textpage.get_text_bounded(rect[0], rect[1], rect[2], rect[3]) print(text)得到文本块的位置后，就可以使用布局分析算法对版面进行分析，比如行检测（通过 Y 坐标差异判断是否在同一行）、列检测（通过 X 坐标差异判断是否属于同一列）或表格边界检测等等。此外，PDFium 还提供了一个 render() 方法，可以方便地将 PDF 转换为图片：bitmap = page.render( scale = 1, # 72dpi resolution rotation = 0, # no additional rotation ) pil_image = bitmap.to_pil() pil_image.save('x.png')pdfplumberpdfplumber 是另一个用于解析 PDF 的 Python 库，它基于 pdfminer.six 构建，但提供了更简洁的 API 和对复杂布局（尤其是表格）的更好支持。它的核心功能如下：文本提取：提取 PDF 中的文本（包括位置、字体等元数据），支持按页面、按区域或按特定条件提取；表格提取：自动检测和提取 PDF 中的表格数据（支持合并单元格、多行文本等复杂结构）；页面操作：获取页面尺寸、裁剪页面区域，提取页面内的图像、线条、矩形等可视化元素；可视化调试：支持绘制页面中的文本、线条、表格框等元素，帮助用户调试提取逻辑。pdfplumber 的基本用法如下：import pdfplumber path = "./pdfs/example.pdf" with pdfplumber.open(path) as pdf: for i in range(len(pdf.pages)): print('----- Page %d -----' % (i+1)) page = pdf.pages[i] text = page.extract_text() print(text)pdfplumber 中有很多借鉴 pdfminer.six 的概念，比如通过 page.chars 可以访问页面中所有的文本字符，获取字体，大小，位置等信息：page = pdf.pages[1] for char in page.chars: print(char['text'], char['fontname'], char['size'], char['x0'], char['y0'], char['x1'], char['y1'])除此之外，通过 page 还能访问很多其他对象，比如 .lines 代表线，.rects 代表矩形框，.curves 代表曲线（无法识别成线或矩形的连接点），.images 代表图像，.annots 代表 PDF 批注，.hyperlinks 代表链接等。布局分析pdfplumber 基于 pdfminer.six 构建，所以也具备 pdfminer.six 布局分析的功能，打开 PDF 时传入 laparams 参数即可：laparams = { "line_overlap": 0.5, "char_margin": 2.0, "line_margin": 0.5, "word_margin": 0.1, } with pdfplumber.open(path, laparams=laparams) as pdf: pass表格提取pdfplumber 的一大亮点是它的表格提取功能，可以处理比较复杂布局的表格。它的基本用法如下：page = pdf.pages[2] tables = page.extract_tables() for table in tables: print(table)得到的表格是一个二维数组，类似下面这样：[ ['姓名', '学号', '学科', '成绩'], ['小明', '001', '语文', '98'], ['小明', '001', '数学', '97'], ['小华', '002', '语文', '94'], ['小华', '002', '数学', '99'], ['小红', '003', '语文', '100'], ['小红', '003', '数学', '95'] ]可以直接加载到 pandas 的 DataFrame 中对表格数据进行处理：import pandas as pd df = pd.DataFrame(table[1:], columns=table[0]) print(df)pdfplumber 的表格提取算法参考了 Anssi Nurminen 的这篇论文 《Algorithmic Extraction of Data in Tables in PDF Documents》，大概的思路如下：首先查找页面中明确定义的行，或者根据单词对齐找到隐含的行；合并重叠或接近重叠的线条；找出这些线条的交点；找到使用这些交点作为顶点的最细粒度的矩形，即单元格；将连续单元格分组到表中。和布局分析类似，这种基于规则的算法必然也提供了大量的规则参数，可以通过 table_settings 传入：table_settings = { "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, "join_tolerance": 3 } tables = page.extract_tables(table_settings=table_settings) for table in tables: print(table)官方仓库里提供了几个示例可供参考，其中 extract-table-ca-warn-report.ipynb 演示了表格提取的基本操作，extract-table-nics.ipynb 演示如何使用可视化调试查找最佳的表提取设置。可视化调试pdfplumber 的另一大亮点是它可以将页面转换为 PageImage 对象（有趣的是，这一步是通过 pypdfium2 实现的），然后在 PageImage 将 chars、lines、rects 绘制出来，通过可视化页面，可以更直观地理解页面的布局结构。绘制文本解析结果：im = page.to_image() im.draw_rects(page.extract_words()).save('extract_words.png')绘制表格解析结果：im = page.to_image() im.debug_tablefinder(table_settings={}).save('debug_tablefinder.png')PyMuPDFMuPDF 是由 Artifex Software 公司开发的一个专注于 PDF 文档处理和渲染的开源库和工具集，具有轻量级、高性能的特点，适合嵌入式系统、移动设备和桌面应用集成。MuPDF 提供了多种语言接口，可以在 JavaScript、Java、.NET 和 Python 等语言中调用，PyMuPDF 就是 MuPDF 的 Python 绑定。注意，MuPDF 采用 AGPL 许可证，商业用途需联系 Artifex 获取商业授权。如果安装 PyMuPDF 时遇到编译错误，可以考虑如下方式安装：$ pip install --only-binary=pymupdf pymupdf下面是 PyMuPDF 的基本用法：import pymupdf doc = pymupdf.open("./pdfs/example.pdf") number_of_pages = len(doc) print('Total %d pages.' % (number_of_pages)) for i in range(number_of_pages): print('----- Page %d -----' % (i+1)) page = doc[i] text = page.get_text() print(text)其中，get_text() 用于从页面中提取文本，默认是以纯文本格式输出，还支持很多 其他的输出格式，比如以 HTML 格式输出：html = page.get_text('html')或以 JSON 格式输出，当我们想对 PDF 结构进一步分析时非常有用：json = page.get_text('json')PyMuPDF 的功能非常丰富，除了 PDF，它还支持大量其他的文件格式，包括 XPS、EPUB、MOBI 等电子书格式，以及 DOCX、XLSX、PPTX 等 Office 文档（Pro 版），这里 有一个表格列举了它的所有特性，并和其他几个 PDF 库进行对比。表格提取PyMuPDF 支持提取 PDF 中的表格数据：from pprint import pprint page = doc[2] for t in page.find_tables(): table = t.extract() pprint(table)和 pdfplumber 一样，得到的表格是一个二维数组，可以直接加载到 pandas 的 DataFrame 中对表格数据进行处理。Markdown 提取在 RAG 中，为了实现文档内容的切片，文档版式分析的准确性显得至关重要，Markdown 作为一种语法简单、段落清晰的文本格式，在 LLM 和 RAG 场景下经常被使用。PyMuPDF 支持将 PDF 文件转换为 Markdown 格式，方便在 LLM 和 RAG 场景下使用，这需要安装 PyMuPDF4LLM 扩展库，它的功能特性如下：支持保留 PDF 中的大多数格式，比如表格、图片、链接、标题、段落、粗体、斜体、列表、代码块等都以 Markdown 语法保留下来；支持多列页面；支持图像和矢量图形提取，在 Markdown 文件中以图片语法；支持按页面分块输出；支持将输出作为 LlamaIndex Documents，方便在 LlamaIndex 中快速开发 RAG 应用；PyMuPDF4LLM 扩展库的安装很简单：$ pip install pymupdf4llm使用也很简单：import pymupdf4llm md_text = pymupdf4llm.to_markdown("./pdfs/example.pdf", write_images=True) print(md_text)官方提供了 一些示例，可以实现 Chat PDF 的功能。OCR 识别PyMuPDF 集成了对 OCR 的支持，可以使用 OCR 对图片或文档页面进行识别，要使用该功能，需要先安装 Tesseract-OCR：$ apt update $ apt install tesseract-ocr默认 Tesseract-OCR 只能识别英文，为了支持中文识别，还需要下载中文的训练数据，可以从 tesseract-ocr/tessdata 仓库下载 chi_sim.traineddata 文件，并将文件放在 /usr/share/tesseract-ocr/5/tessdata/ 这个目录下。PyMuPDF 支持两种 OCR 策略，第一种是识别整个页面，第二种是提取页面中的图片进行识别。由于 OCR 非常缓慢，所以建议在处理 PDF 时每页只进行一次 OCR，当出现下面这些情况时可以使用第一种策略：页面完全被图片覆盖；页面上不存在文本；页面上有数千个小矢量图形；示例代码如下：page = doc[3] textpage = page.get_textpage_ocr() text = textpage.extractText() print(text)这里通过 page.get_textpage_ocr() 方法将 Page 转换为 TextPage 从而方便后续我们提取文本。第二种策略是识别单独的图片，我们先从页面中将图片提取出来：page = doc[3] for img in page.get_images(): xref = img[0] data = doc.extract_image(xref)['image'] pix = pymupdf.Pixmap(data)这里先通过 page.get_images() 获取页面中的所有图片，注意返回的是图片的 xref 引用信息，需要通过 doc.extract_image() 提取出图片的原始数据，最后再转换为 Pixmap 格式，只有这种格式才能 OCR 识别。得到 Pixmap 之后，我们可以将其保存成本地文件： pix.save('x.png')也可以对其进行 OCR 识别： pix.pdfocr_save('x.pdf', language='eng')这个方法将 Pixmap 保存为 1 页的 PDF 文件，该页面看起来像原始图片，具有相同的宽度和高度，但是它包含了一个被 Tesseract 识别的文本层，可以使用常规文本提取和搜索方法进行提取和搜索。当然这样做比较繁琐，会生成大量的临时文件，我们也可以直接在内存中进行读取： bytes = pix.pdfocr_tobytes() imgdoc = pymupdf.open("pdf", bytes) page = imgdoc[0] text = page.get_text() print(text)还有另一种方式可以将 Page 转换成 Pixmap：page = doc[0] pix = page.get_pixmap(dpi=300) pix.save('x.png') pix.pdfocr_save('x.pdf', language='chi_sim')注意这里的 dpi=300 参数，可以保证生成的图片足够清晰，模糊的图片可能导致 Tesseract 识别失败。Tabula vs. Camelot上面提到 pdfplumber 和 PyMuPDF 都具备表格提取功能，关于表格提取，还有两个库不得不提，那就是 Tabula 和 Camelot。Tabula 是一款专门用于提取 PDF 中表格的工具，它本身是一个基于 Java 开发的本地 Web 应用。首先上传 PDF 文件，手动选择或自动检测表格区域：然后将选择的表格区域提取成结构化的数据：Tabula 实现了两种表格提取算法：一种叫 Stream 模式（参考 BasicExtractionAlgorithm 和 NurminenDetectionAlgorithm，实际上也是借鉴了 Anssi Nurminen 那篇论文中的思路），另一种叫 Lattice 模式（参考 SpreadsheetExtractionAlgorithm）；Stream 模式通过查找列之间的空白来确定表格，而 Lattice 模式则是查找列之间的边界线，如果单元格之间有明确的边界线，建议选择 Lattice 模式，当其中一种算法无法提取表格数据时，可以切换到另一种尝试。同时，Tabula 也开源了对应的 SDK 方便其他开发者集成，核心仓库是 tabula-java，也有社区维护的 Python 绑定 tabula-py，基本用法如下：import tabula dfs = tabula.read_pdf( "./pdfs/table.pdf", pages = '1' ) print(dfs[0])注意：由于 Tabula 是基于 Java 实现的，所以要安装 JVM 环境。Camelot 是另一款 PDF 表格提取工具和库，和 Tabula 非常类似，它是纯 Python 实现，不用依赖 JVM 环境。而且相比于 Tabula 它提供了更多的 高级选项，能应付更多更复杂的场景。这里 有一份 Camelot 和 Tabula 的详细对比，根据对比结果，Camelot 在多个场景下效果都比 Tabula 要好。Camelot 用起来和 Tabula 也很像，如下：import camelot tables = camelot.read_pdf( './pdfs/table.pdf', pages = "1", flavor = "stream" ) print(tables[0].df)其中 flavor = "stream" 表示使用 Stream 模式，同样的，Camelot 也支持 Lattice 模式，不过它的实现有些不同（参考 lattice.py），它首先通过 Ghostscript 或 Pdfium 将 PDF 页面转换为图像，然后使用 OpenCV 的图像学算法，获取水平和垂直线段；推荐阅读 Camelot 的官方文档 How It Works，这篇文档带我们详细了解 Lattice 是如何一步一步地处理 PDF 页面的。此外，这篇文档还介绍了 Camelot 的另两种模式：Network 模式 和 Hybrid 模式，此处不再赘述。除了 Camelot 和 Tabula，还有很多类似的库也用于表格提取，比如 pdftables 和 pdf-table-extract 等。pikepdfpikepdf 是另一个 Python PDF 解析库，帮助开发者读取、写入和修改 PDF 文件。这个库的取名很有意思：Python + qpdf = "py" + "qpdf" = "pyqpdf"可以看出 pikepdf 基于 qpdf 开发，而 qpdf 是一个使用 C++ 编写的功能强大的 PDF 操作和修复库。使用 pikepdf 解析 PDF 不是一件容易的事，因为它使用了一些比较低级的概念，比如 对象模型 和 流，这种设计使得用户需要对 PDF 的内部结构和规范有一定的了解，才能高效地使用该库。当我们准备上手 pikepdf 的入门示例时，我们会发现它甚至都 没有提取文本的能力，只能访问页面中的 内容流（Content Streams）：import pikepdf with pikepdf.open('./pdfs/example.pdf') as pdf: page = pdf.pages[0] instructions = pikepdf.parse_content_stream(page) data = pikepdf.unparse_content_stream(instructions) print(data.decode()) 解析出来的结果类似于下面这样的被称为 指令（Instructions） 的特殊字符串：q 1 0 0 -1 0 841.9 cm q 0 0 595.3 841.9 re W* n 0.12 w 2 M 2 J 2 j 0 0 0 RG对 PDF 底层数据结构感兴趣的同学可以看看 《PDF Explained》 这本电子书。强烈不推荐从内容流中解析文本，因为这意味着你需要手动处理字体、字号、编码等一系列问题，这只适合部分硬核玩家，我们不如直接使用其他现成的 PDF 解析库。不过我们可以使用 pikepdf 处理图片，比如提取、替换、删除等，下面是提取图片的一个示例： page = pdf.pages[3] for key in page.images: rawimage = page.images[key] pdfimage = pikepdf.PdfImage(rawimage) pdfimage.extract_to(fileprefix='x')OCRmyPDFOCRmyPDF 的特点是通过 OCR 识别图像中包含的文本，并为 PDF 中的图像添加一个文本层，使得图像 PDF 也可以被搜索。它的工作流程大致如下：首先通过 pikepdf 处理 PDF，修复 PDF 中可能存在的语法错误，并将 PDF 拆分成单页；然后使用 Ghostscript 以 -dSAFER 模式对 PDF 的每一页进行光栅化；光栅图也叫做位图、点阵图、像素图，可以方便的进行 OCR 识别；接着使用 Tesseract 对光栅化的图像执行 OCR 识别；最后将识别的结果和原始的 PDF 进行合成，生成一个新的 PDF 文件。默认情况下，OCRmyPDF 生成的文件是 PDF/A 格式，这是 PDF 规范的子集，旨在用于归档（“A” 代表归档）。PDF/A 与 PDF 的主要区别在于去掉了很多复杂的功能，例如嵌入式 Javascript、视频、音频和对外部字体的引用，因此它可能更安全。OCRmyPDF 可以通过命令行工具使用，也可以通过 API 集成在 Python 代码中：import ocrmypdf ocrmypdf.ocr('./pdfs/example.pdf', 'output.pdf', skip_text=True)正如前文所述，OCRmyPDF 依赖于 Ghostscript 处理页面，所以我们需要先安装 Ghostscript，不过当前使用 apt install ghostscript 命令安装的是 10.0.0 版本，是个有问题的版本，无法在 OCRmyPDF 中直接使用。如果你也遇到和我一样的问题，可以通过下面的步骤手动编译源码来安装：$ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs10040/ghostscript-10.04.0.tar.gz $ tar zxvf ghostscript-10.04.0.tar.gz $ cd ghostscript-10.04.0 $ ./configure $ make install上面的 skip_text=True 参数表示跳过有文本的页面，只处理有图片的页面。要注意的是，如果页面既有文本又有图片，也会跳过，可以设置 force_ocr=True 参数强制识别所有页面：ocrmypdf.ocr('./pdfs/example.pdf', 'output.pdf', force_ocr=True)详细的参数说明可以参考官网的使用手册。打开生成的 PDF 文件，如果一切正常，可以发现图片上的文字不仅可以搜索，也可以用鼠标框选复制（而且框选的位置和图片中文字的位置非常吻合，一点违和感都没有）：此外，OCRmyPDF 通过 pluggy 实现了插件机制，我们可以基于插件机制实现如下功能：添加新的命令行参数；执行 OCR 之前增加自己的判断逻辑；在 OCR 识别或 PDF 生成之前对图片进行修改；用其他 OCR 引擎替换 Tesseract OCR；用其他光栅化或 PDF/A 生成器替换 Ghostscript；感兴趣的同学可以参考官网的插件文档。markitdownmarkitdown 是微软开源的一款 Python 库，旨在将各种文件格式转换为 Markdown。该库的一大特色是支持大量的文件格式，包括：Word、PPT、Excel、PDF、HTML、JSON、XML、CSV、ZIP、图像、音频、URL，等等等等，因此人气非常高，目前在 Github 上收获了超过 39k 的 Star。使用 markitdown 非常简单，只需要 4 行代码：from markitdown import MarkItDown md = MarkItDown() result = md.convert("./pdfs/example.docx") print(result.text_content)上面的代码将 Word 文档转换为 Markdown 格式，支持保留标题、表格、图片等 Markdown 语法，对 LLM 和 RAG 场景非常友好。深入研究 markitdown 的源码可以发现，它集众家之所长，使用了大量其他流行的开源库，比如：通过 mammoth 将 Word 文件转换为 HTML 格式；通过 pandos 和 openpyxl 将 Excel 文件转换为 HTML 格式；通过 pptx 解析 PPT 文件；通过 markdownify 将 HTML 转换为 Markdown；通过 pdfminer.six 解析 PDF 文件；通过 speech_recognition 对音频文件进行转译；尽管 markitdown 支持这么多的文件格式，但是它对 PDF 的解析效果并不好，它是直接使用 pdfminer.high_level.extract_text() 提取 PDF 中的文本的，所以标题、表格、图片等格式都丢失了，期待 markitdown 能稍微加强下对 PDF 的支持，能做到和 pymupdf4llm 一样的效果就完美了。markitdown 的另一大特色是对图片的处理，不同于其他库喜欢用 Tesseract 做 OCR 识别，它通过多模态大模型（比如 gpt-4o、Qwen-VL 等）为图片生成详细描述：from openai import Client client = Client() md = MarkItDown(llm_client=client, llm_model="gpt-4o") result = md.convert("./pdfs/example.jpg") print(result.text_content)gptpdfgptpdf 是另一个使用多模态大模型来解析 PDF 的库，它的核心思想非常简单：将 PDF 文件的每一页转换为图片，然后丢给大模型，让它将图片中识别到的文字转换为 Markdown 格式输出。它的 核心代码 不过 300 行，但几乎可以完美地解析排版、数学公式、表格、图片、图表等。内置的 Prompt 也很简单：使用markdown语法，将图片中识别到的文字转换为markdown格式输出。你必须做到： 1. 输出和使用识别到的图片的相同的语言，例如，识别到英语的字段，输出的内容必须是英语。 2. 不要解释和输出无关的文字，直接输出图片中的内容。例如，严禁输出 “以下是我根据图片内容生成的markdown文本：”这样的例子，而是应该直接输出markdown。 3. 内容不要包含在```markdown ```中、段落公式使用 $$ $$ 的形式、行内公式使用 $ $ 的形式、忽略掉长直线、忽略掉页码。 再次强调，不要解释和输出无关的文字，直接输出图片中的内容。gptpdf 使用了作者自己开发的 GeneralAgent 库来调用大模型，支持 GPT-4o、Qwen-VL、GLM-4V、Yi-VL 等：from gptpdf import parse_pdf content, image_paths = parse_pdf( pdf_path = "./pdfs/text+image.pdf", output_dir = "./out", model = "gpt-4o", ) print(content)上面的代码运行成功后，会将输出结果写入到 out 目录下的 output.md 文件。如果 PDF 中含有图片，gptpdf 的处理方式稍微有些不一样，它会首先使用 PyMuPDF 将图片区域用一个红色的框标记出来，像下面这样：然后将这个区域裁剪成子图保存起来，并让大模型在识别到这个图片时使用 Markdown 的图片语法 ![]() 替换该位置，内置的 Prompt 会加上如下内容：图片中用红色框和名称(%s)标注出了一些区域。如果区域是表格或者图片，使用 ![]() 的形式插入到输出内容中，否则直接输出文字内容。 0_0.png, 0_1.png参考pypdf's documentationpdfminer.six’s documentationpypdfium2's documentationPyMuPDF's documentationTabula: Extract Tables from PDFsCamelot: PDF Table Extraction for Humanspikepdf's documentationOCRmyPDF's documentation文档处理之10种PDF解析工具测评MarkItDown深入研究
在 APISIX 使用小记 中，我们通过 APISIX 官方提供的入门示例学习了 APISIX 的基本概念，并使用 Admin API 和 Dashboard 两种方法创建路由。在创建路由时，我们必须明确地知道服务的 IP 和端口，这给运维人员带来了一定的负担，因为服务的重启或扩缩容都可能会导致服务的 IP 和端口发生变化，当服务数量非常多的时候，维护成本将急剧升高。APISIX 集成了多种服务发现机制来解决这个问题，通过服务注册中心，APISIX 可以动态地获取服务的实例信息，这样我们就不用在路由中写死固定的 IP 和端口了。如下图所示，一个标准的服务发现流程大致包含了三大部分：服务启动时将自身的一些信息，比如服务名、IP、端口等信息上报到注册中心；各个服务与注册中心使用一定机制（例如心跳）通信，如果注册中心与服务长时间无法通信，就会注销该实例；当服务下线时，会删除注册中心的实例信息；网关会准实时地从注册中心获取服务实例信息；当用户通过网关请求服务时，网关从注册中心获取的实例列表中选择一个进行代理；目前市面上流行着很多注册中心，比如 Eureka、Nacos、Consul 等，APISIX 内置了下面这些服务发现机制：基于 Eureka 的服务发现基于 Nacos 的服务发现基于 Consul 的服务发现基于 Consul KV 的服务发现基于 DNS 的服务发现基于 APISIX-Seed 架构的控制面服务发现基于 Kubernetes 的服务发现基于 Eureka 的服务发现Eureka 是 Netflix 开源的一款注册中心服务，它也被称为 Spring Cloud Netflix，是 Spring Cloud 全家桶中的核心成员。本节将演示如何让 APISIX 通过 Eureka 来实现服务发现，动态地获取下游服务信息。启动 Eureka Server我们可以直接运行官方的示例代码 spring-cloud-samples/eureka 来启动一个 Eureka Server：$ git clone https://github.com/spring-cloud-samples/eureka.git $ cd eureka && ./gradlew bootRun或者也可以直接使用官方制作好的镜像：$ docker run -d -p 8761:8761 springcloud/eureka启动之后访问 http://localhost:8761/ 看看 Eureka Server 是否已正常运行。启动 Eureka Client如果一切顺利，我们再准备一个简单的 Spring Boot 客户端程序，引入 spring-cloud-starter-netflix-eureka-client 依赖，再通过 @EnableEurekaClient 注解将服务信息注册到 Eureka Server：@EnableEurekaClient @SpringBootApplication @RestController public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } @RequestMapping("/") public String home() { return String.format("Hello, I'm eureka client."); } }在配置文件中设置服务名称和服务端口：spring.application.name=eureka-client server.port=8081默认注册的 Eureka Server 地址是 http://localhost:8761/eureka/，可以通过下面的参数修改：eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/默认情况下，Eureka Client 是将该服务所在的主机名注册到 Eureka Server，这在某些情况下可能会导致其他服务调不通该服务，我们可以通过下面的参数，让 Eureka Client 注册 IP 地址：eureka.instance.prefer-ip-address=true eureka.instance.ip-address=192.168.1.40启动后，在 Eureka 页面的实例中可以看到我们注册的服务：APISIX 集成 Eureka 服务发现接下来，我们要让 APISIX 通过 Eureka Server 找到我们的服务。首先，在 APISIX 的配置文件 config.yaml 中添加如下内容：discovery: eureka: host: - "http://192.168.1.40:8761" prefix: /eureka/然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/11 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/eureka", "plugins": { "proxy-rewrite" : { "regex_uri": ["/eureka", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "eureka", "service_name": "EUREKA-CLIENT" } }'之前创建路由时，我们在 upstream 中通过 nodes 参数表示上游服务器节点，这里我们不再需要写死服务器节点信息，而是通过 "discovery_type": "eureka" 和 "service_name": "EUREKA-CLIENT" 来让 APISIX 使用 eureka 服务发现机制，上游的服务名称为 EUREKA-CLIENT。值得注意的是，虽然上面的 Eureka Client 的 spring.application.name 是小写，但是注册到 Eureka Server 的服务名称是大写，所以这里的 service_name 参数必须是大写。此外，这里我们还使用了 proxy-rewrite 插件，它相当于 Nginx 中的路径重写功能，当多个上游服务的接口地址相同时，通过路径重写可以将它们区分开来。访问 APISIX 的 /eureka 地址验证一下：$ curl http://127.0.0.1:9080/eureka Hello, I'm eureka client.我们成功通过 APISIX 访问到了我们的服务。关于 APISIX 集成 Eureka 的更多信息，可以参考官方文档 集成服务发现注册中心 和官方博客 API 网关 Apache APISIX 集成 Eureka 作为服务发现。基于 Nacos 的服务发现Nacos 是阿里开源的一款集服务发现、配置管理和服务管理于一体的管理平台，APISIX 同样支持 Nacos 的服务发现机制。启动 Nacos Server首先，我们需要准备一个 Nacos Server，Nacos 官网提供了多种部署方式，可以 通过源码或安装包安装、通过 Docker 安装 或 通过 Kubernetes 安装，我们这里直接使用 docker 命令启动一个本地模式的 Nacos Server：$ docker run -e MODE=standalone -p 8848:8848 -p 9848:9848 -d nacos/nacos-server:v2.2.0不知道为什么，有时候启动会报这样的错误：com.alibaba.nacos.api.exception.runtime.NacosRuntimeException: errCode: 500, errMsg: load derby-schema.sql error，多启动几次又可以了。启动成功后，访问 http://localhost:8848/nacos/ 进入 Nacos 管理页面，默认用户名和密码为 nacos/nacos：启动 Nacos Client接下来，我们再准备一个简单的 Spring Boot 客户端程序，引入 nacos-discovery-spring-boot-starter 依赖，并通过它提供的 NameService 将服务信息注册到 Nacos Server：@SpringBootApplication @RestController public class NacosApplication implements CommandLineRunner { @Value("${spring.application.name}") private String applicationName; @Value("${server.port}") private Integer serverPort; @NacosInjected private NamingService namingService; public static void main(String[] args) { SpringApplication.run(NacosApplication.class, args); } @Override public void run(String... args) throws Exception { namingService.registerInstance(applicationName, "192.168.1.40", serverPort); } @RequestMapping("/") public String home() { return String.format("Hello, I'm nacos client."); } }在配置文件中设置服务名称和服务端口：spring.application.name=nacos-client server.port=8082以及 Nacos Server 的地址：nacos.discovery.server-addr=127.0.0.1:8848启动后，在 Nacos 的服务管理页面中就可以看到我们注册的服务了：APISIX 集成 Nacos 服务发现接下来，我们要让 APISIX 通过 Nacos Server 找到我们的服务。首先，在 APISIX 的配置文件 config.yaml 中添加如下内容：discovery: nacos: host: - "http://192.168.1.40:8848" prefix: "/nacos/v1/"然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/22 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/nacos", "plugins": { "proxy-rewrite" : { "regex_uri": ["/nacos", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "nacos", "service_name": "nacos-client" } }'和上面 Eureka 服务发现的例子一样，我们也使用 proxy-rewrite 插件实现了路径重写功能，访问 APISIX 的 /nacos 地址验证一下：$ curl http://127.0.0.1:9080/nacos Hello, I'm nacos client.我们成功通过 APISIX 访问到了我们的服务。关于 APISIX 集成 Nacos 的更多信息，可以参考官方文档 基于 Nacos 的服务发现 和官方博客 Nacos 在 API 网关中的服务发现实践。基于 Consul 的服务发现Consul 是由 HashiCorp 开源的一套分布式系统的解决方案，同时也可以作为一套服务网格解决方案，提供了丰富的控制平面功能，包括：服务发现、健康检查、键值存储、安全服务通信、多数据中心等。启动 Consul ServerConsul 使用 Go 语言编写，安装和部署都非常简单，官方提供了 Consul 的多种安装方式，包括 二进制安装、Kubernetes 安装 或 HCP 安装。这里我们使用最简单的二进制安装方式，这种方式只需要执行一个可执行文件即可，首先，我们从 Install Consul 页面找到对应操作系统的安装包并下载：$ curl -LO https://releases.hashicorp.com/consul/1.15.1/consul_1.15.1_linux_amd64.zip $ unzip consul_1.15.1_linux_amd64.zip下载并解压之后，Consul 就算安装成功了，使用 consul version 命令进行验证：$ ./consul version Consul v1.15.1 Revision 7c04b6a0 Build Date 2023-03-07T20:35:33Z Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)Consul 安装完成后，就可以启动 Consul Agent 了，Consul Agent 有 -server 和 -client 两种模式，-client 一般用于服务网格等场景，这里我们通过 -server 模式启动：$ ./consul agent -server -ui -bootstrap-expect=1 -node=agent-one -bind=127.0.0.1 -client=0.0.0.0 -data-dir=./data_dir ==> Starting Consul agent... Version: '1.15.1' Build Date: '2023-03-07 20:35:33 +0000 UTC' Node ID: '8c1ccd5a-69b3-4c95-34c1-f915c19a3d08' Node name: 'agent-one' Datacenter: 'dc1' (Segment: '<all>') Server: true (Bootstrap: true) Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, gRPC: -1, gRPC-TLS: 8503, DNS: 8600) Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302) Gossip Encryption: false Auto-Encrypt-TLS: false HTTPS TLS: Verify Incoming: false, Verify Outgoing: false, Min Version: TLSv1_2 gRPC TLS: Verify Incoming: false, Min Version: TLSv1_2 Internal RPC TLS: Verify Incoming: false, Verify Outgoing: false (Verify Hostname: false), Min Version: TLSv1_2 ==> Log data will now stream in as it occurs:其中 -ui 表示开启内置的 Web UI 管理界面，-bootstrap-expect=1 表示服务器希望以 bootstrap 模式启动，-node=agent-one 用于指定节点名称，-bind=127.0.0.1 这个地址用于 Consul 集群内通信，-client=0.0.0.0 这个地址用于 Consul 和客户端之间的通信，包括 HTTP 和 DNS 两种通信方式，-data-dir 参数用于设置数据目录。关于 consul agent 更多的命令行参数，可以参考 Agents Overview 和 Agents Command-line Reference。简单起见，我们也可以使用 -dev 参数以开发模式启动 Consul Agent：$ ./consul agent -dev如果 Consul Agent 启动成功，访问 http://localhost:8500/ 进入 Consul 的管理页面，在服务列表可以看到 consul 这个服务：在节点列表可以看到 agent-one 这个节点：启动 Consul Client让我们继续编写 Consul Client 程序，引入 spring-cloud-starter-consul-discovery 依赖，并通过 @EnableDiscoveryClient 注解将服务信息注册到 Consul Server：@EnableDiscoveryClient @SpringBootApplication @RestController public class ConsulApplication { public static void main(String[] args) { SpringApplication.run(ConsulApplication.class, args); } @RequestMapping("/") public String home() { return String.format("Hello, I'm consul client."); } }可以看到和 Eureka Client 的代码几乎是完全一样的，不过有一点要注意，我们还需要在 pom.xml 文件中引入 spring-boot-starter-actuator 依赖，开启 Actuator 端点，因为 Consul 默认是通过 /actuator/health 接口来对程序做健康检查的。在配置文件中设置服务名称和服务端口：spring.application.name=consul-client server.port=8083以及 Consul 相关的配置：spring.cloud.consul.host=127.0.0.1 spring.cloud.consul.port=8500 spring.cloud.consul.discovery.service-name=${spring.application.name} spring.cloud.consul.discovery.prefer-ip-address=true spring.cloud.consul.discovery.ip-address=192.168.1.40启动后，在 Consul 的服务管理页面中就可以看到我们注册的服务了：APISIX 集成 Consul 服务发现接下来，我们要让 APISIX 通过 Consul Server 找到我们的服务。首先，在 APISIX 的配置文件 config.yaml 中添加如下内容：discovery: consul: servers: - "http://192.168.1.40:8500"然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/33 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/consul", "plugins": { "proxy-rewrite" : { "regex_uri": ["/consul", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "consul", "service_name": "consul-client" } }'访问 APISIX 的 /consul 地址验证一下：$ curl http://127.0.0.1:9080/consul Hello, I'm consul client.关于 APISIX 集成 Consul 的更多信息，可以参考官方文档 基于 Consul 的服务发现。基于 Consul KV 的服务发现Consul 还提供了分布式键值数据库的功能，这个功能和 Etcd、ZooKeeper 类似，主要用于存储配置参数和元数据。基于 Consul KV 我们也可以实现服务发现的功能。首先准备 consul-kv-client 客户端程序，它的地址为 192.168.1.40:8084，我们通过 Consul KV 的 HTTP API 手工注册服务：$ curl -X PUT http://127.0.0.1:8500/v1/kv/upstreams/consul-kv-client/192.168.1.40:8084 -d ' {"weight": 1, "max_fails": 2, "fail_timeout": 1}'其中，/v1/kv/ 后的路径按照 {Prefix}/{Service Name}/{IP}:{Port} 的格式构成。可以在 Consul 的 Key/Value 管理页面看到我们注册的服务：然后在 APISIX 的配置文件 config.yaml 中添加如下内容：discovery: consul_kv: servers: - "http://192.168.1.40:8500" prefix: "upstreams"然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/44 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/consul_kv", "plugins": { "proxy-rewrite" : { "regex_uri": ["/consul_kv", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "consul_kv", "service_name": "http://192.168.1.40:8500/v1/kv/upstreams/consul-kv-client/" } }'注意这里的 service_name 参数需要设置为 KV 的 URL 路径，访问 APISIX 的 /consul_kv 地址验证一下：$ curl http://127.0.0.1:9080/consul_kv Hello, I'm consul_kv client.另一点需要注意的是，这种方式注册的服务没有健康检查机制，服务退出后需要手工删除对应的 KV：$ curl -X DELETE http://127.0.0.1:8500/v1/kv/upstreams/consul-kv-client/192.168.1.40:8084关于 APISIX 集成 Consul KV 的更多信息，可以参考官方文档 基于 Consul KV 的服务发现 和官方博客 Apache APISIX 集成 Consul KV，服务发现能力再升级。基于 DNS 的服务发现Consul 不仅支持 HTTP API，而且还支持 DNS API，它内置了一个小型的 DNS 服务器，默认端口为 8600，我们以上面的 consul-client 为例，介绍如何在 APISIX 中集成 DNS 的服务发现。注册到 Consul 中的服务默认会在 Consul DNS 中添加一条 <服务名>.service.consul 这样的域名记录，使用 dig 命令可以查询该域名的信息：$ dig @192.168.1.40 -p 8600 consul-client.service.consul ; <<>> DiG 9.11.3-1ubuntu1.17-Ubuntu <<>> @192.168.1.40 -p 8600 consul-client.service.consul ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32989 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;consul-client.service.consul. IN A ;; ANSWER SECTION: consul-client.service.consul. 0 IN A 192.168.1.40 ;; Query time: 4 msec ;; SERVER: 192.168.1.40#8600(192.168.1.40) ;; WHEN: Tue Mar 21 07:17:40 CST 2023 ;; MSG SIZE rcvd: 73上面的查询结果中只包含 A 记录，A 记录中只有 IP 地址，没有服务端口，如果用 A 记录来做服务发现，服务的端口必须得固定；好在 Consul 还支持 SRV 记录，SRV 记录中包含了服务的 IP 和端口信息：$ dig @192.168.1.40 -p 8600 consul-client.service.consul SRV ; <<>> DiG 9.11.3-1ubuntu1.17-Ubuntu <<>> @192.168.1.40 -p 8600 consul-client.service.consul SRV ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41141 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 3 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;consul-client.service.consul. IN SRV ;; ANSWER SECTION: consul-client.service.consul. 0 IN SRV 1 1 8083 c0a80128.addr.dc1.consul. ;; ADDITIONAL SECTION: c0a80128.addr.dc1.consul. 0 IN A 192.168.1.40 agent-one.node.dc1.consul. 0 IN TXT "consul-network-segment=" ;; Query time: 3 msec ;; SERVER: 192.168.1.40#8600(192.168.1.40) ;; WHEN: Tue Mar 21 07:18:22 CST 2023 ;; MSG SIZE rcvd: 168我们在 APISIX 的配置文件 config.yaml 中添加如下内容：discovery: dns: servers: - "192.168.1.40:8600"然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/55 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/dns", "plugins": { "proxy-rewrite" : { "regex_uri": ["/dns", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "dns", "service_name": "consul-client.service.consul" } }'访问 APISIX 的 /dns 地址验证一下：$ curl http://127.0.0.1:9080/dns Hello, I'm consul client.关于 Consul DNS 的更多信息，可以参考官方文档 Discover services with DNS，除了 Consul DNS，我们也可以使用其他的 DNS 服务器来做服务发现，比如 CoreDNS 就是 Kubernetes 环境下的服务发现默认实现。关于 APISIX 集成 DNS 的更多信息，可以参考官方文档 基于 DNS 的服务发现 和官方博客 API 网关 Apache APISIX 携手 CoreDNS 打开服务发现新大门。基于 APISIX-Seed 架构的控制面服务发现上面所介绍的所有服务发现机制都是在 APISIX 上进行的，我们需要修改 APISIX 的配置，并重启 APISIX 才能生效，这种直接在网关上实现的服务发现也被称为 数据面服务发现，APISIX 还支持另一种服务发现机制，称为 控制面服务发现。控制面服务发现不直接对 APISIX 进行修改，而是将服务发现结果保存到 Etcd 中，APISIX 实时监听 Etcd 的数据变化，从而实现服务发现：APISIX 通过 APISIX-Seed 项目实现了控制面服务发现，这样做有下面几个好处：简化了 APISIX 的网络拓扑，APISIX 只需要关注 Etcd 的数据变化即可，不再和每个注册中心保持网络连接；APISIX 不用额外存储注册中心的服务数据，减小内存占用；APISIX 的配置变得简单，更容易管理；虽然如此，目前 APISIX-Seed 还只是一个实验性的项目，从 GitHub 上的活跃度来看，官方似乎对它的投入并不是很高，目前只支持 ZooKeeper 和 Nacos 两种服务发现，而且官方也没有提供 APISIX-Seed 安装包的下载，需要我们自己通过源码来构建：$ git clone https://github.com/api7/apisix-seed.git $ make build构建完成后，可以得到一个 apisix-seed 可执行文件，然后我们以上面的 Nacos 为例，介绍如何通过 APISIX-Seed 来实现控制面服务发现。首先，我们将 APISIX 的配置文件中所有服务发现相关的配置都删掉，并重启 APISIX，接着打开 conf/conf.yaml 配置文件，文件中已经提前配置好了 Etcd、ZooKeeper、Nacos 等相关的配置，我们对其做一点裁剪，只保留下面这些信息：etcd: host: - "http://127.0.0.1:2379" prefix: /apisix timeout: 30 log: level: warn path: apisix-seed.log maxage: 168h maxsize: 104857600 rotation_time: 1h discovery: nacos: host: - "http://127.0.0.1:8848" prefix: /nacos然后启动 apisix-seed：$ ./apisix-seed panic: no discoverer with key: dns goroutine 15 [running]: github.com/api7/apisix-seed/internal/discoverer.GetDiscoverer(...) D:/code/apisix-seed/internal/discoverer/discovererhub.go:42 D:/code/apisix-seed/internal/core/components/watcher.go:84 +0x1d4 created by github.com/api7/apisix-seed/internal/core/components.(*Watcher).Init D:/code/apisix-seed/internal/core/components/watcher.go:48 +0x2b6 panic: no discoverer with key: consul goroutine 13 [running]: github.com/api7/apisix-seed/internal/discoverer.GetDiscoverer(...) D:/code/apisix-seed/internal/discoverer/discovererhub.go:42 github.com/api7/apisix-seed/internal/core/components.(*Watcher).handleQuery(0x0?, 0xc000091200, 0x0?) D:/code/apisix-seed/internal/core/components/watcher.go:84 +0x1d4 created by github.com/api7/apisix-seed/internal/core/components.(*Watcher).Init D:/code/apisix-seed/internal/core/components/watcher.go:48 +0x2b6不过由于上面我们在路由中添加了 dns、consul 这些服务发现类型，这些 APISIX-Seed 是不支持的，所以启动会报错，我们需要将这些路由删掉：$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' $ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/33 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' $ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/44 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' $ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/55 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'只保留一条 /nacos 的路由，然后重启 apisix-seed 即可：$ ./apisix-seed 2023-03-22T07:49:53.849+0800 INFO naming_client/push_receiver.go:80 udp server start, port: 55038访问 APISIX 的 /nacos 地址验证一下：$ curl http://127.0.0.1:9080/nacos Hello, I'm nacos client.关于 APISIX-Seed 的更多信息，可以参考官方文档 基于 APISIX-Seed 架构的控制面服务发现 和 APISIX-Seed 项目文档。基于 Kubernetes 的服务发现我们的服务还可能部署在 Kubernetes 集群中，这时，不用依赖外部的服务注册中心也可以实现服务发现，因为 Kubernetes 提供了强大而丰富的监听资源的接口，我们可以通过监听 Kubernetes 集群中 Services 或 Endpoints 等资源的实时变化来实现服务发现，APISIX 就是这样做的。我们以 Kubernetes 使用小记 中的 kubernetes-bootcamp 为例，体验一下 APISIX 基于 Kubernetes 的服务发现。首先在 Kubernetes 集群中创建对应的 Deployment 和 Service：$ kubectl create deployment kubernetes-bootcamp --image=jocatalin/kubernetes-bootcamp:v1 deployment.apps/kubernetes-bootcamp created $ kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080 service/kubernetes-bootcamp exposed通过 kubectl get svc 获取 NodePort 端口，并验证服务能正常访问：$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 115d kubernetes-bootcamp NodePort 10.101.31.128 <none> 8080:32277/TCP 59s $ curl http://192.168.1.40:32277 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-857b45f5bb-jtzs4 | v=1接下来，为了让 APISIX 能查询和监听 Kubernetes 的 Endpoints 资源变动，我们需要创建一个 ServiceAccount：kind: ServiceAccount apiVersion: v1 metadata: name: apisix-test namespace: default以及一个具有集群级查询和监听 Endpoints 资源权限的 ClusterRole：kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: apisix-test rules: - apiGroups: [ "" ] resources: [ endpoints ] verbs: [ get,list,watch ]再将这个 ServiceAccount 和 ClusterRole 关联起来：apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: apisix-test roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: apisix-test subjects: - kind: ServiceAccount name: apisix-test namespace: default然后我们需要获取这个 ServiceAccount 的 token 值，如果 Kubernetes 是 v1.24 之前的版本，可以通过下面的方法获取 token 值：$ kubectl get secrets | grep apisix-test $ kubectl get secret apisix-test-token-c64cv -o jsonpath={.data.token} | base64 -dKubernetes 从 v1.24 版本开始，不能再通过 kubectl get secret 获取 token 了，需要使用 TokenRequest API 来获取，首先开启代理：$ kubectl proxy --port=8001 Starting to serve on 127.0.0.1:8001然后调用 TokenRequest API 生成一个 token：$ curl 'http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/apisix-test/token' \ -H "Content-Type:application/json" -X POST -d '{}' { "kind": "TokenRequest", "apiVersion": "authentication.k8s.io/v1", "metadata": { "name": "apisix-test", "namespace": "default", "creationTimestamp": "2023-03-22T23:57:20Z", "managedFields": [ { "manager": "curl", "operation": "Update", "apiVersion": "authentication.k8s.io/v1", "time": "2023-03-22T23:57:20Z", "fieldsType": "FieldsV1", "fieldsV1": { "f:spec": { "f:expirationSeconds": {} } }, "subresource": "token" } ] }, "spec": { "audiences": [ "https://kubernetes.default.svc.cluster.local" ], "expirationSeconds": 3600, "boundObjectRef": null }, "status": { "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtLdHRyVzFmNTRHWGFVUjVRS3hrLVJMSElNaXM4aENLMnpfSGk1SUJhbVkifQ.[binary].YexM_VoumpdwZNbSkwh6IbEu59PCtZrG1lkTnCqG24G-TC0U1sGxgbXf6AnUQ5ybh-CHWbJ7oewhkg_J4j7FiSAnV_yCcEygLkaCveGIQbWldB3phDlcJ52f8YDpHFtN2vdyVTm79ECwEInDsqKhn4n9tPY4pgTodI6D9j-lcK0ywUdbdlL5VHiOw9jlnS7b60fKWBwCPyW2uohX5X43gnUr3E1Wekgpo47vx8lahTZQqnORahTdl7bsPsu_apf7LMw40FLpspVO6wih-30Ke8CNBxjpORtX2n3oteE1fi2vxYHoyJSeh1Pro_Oykauch0InFUNyEVI4kJQ720glOw", "expirationTimestamp": "2023-03-23T00:57:20Z" } }默认的 token 有效期只有一个小时，可以通过参数改为一年：$ curl 'http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/apisix-test/token' \ -H "Content-Type:application/json" -X POST \ -d '{"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"apisix-test","namespace":"default"},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":31536000}}'我们在 APISIX 的配置文件 config.yaml 中添加如下内容（ 将上面生成的 token 填写到 token 字段 ）：discovery: kubernetes: service: schema: https host: 127.0.0.1 port: "6443" client: token: ...这里有一个比较坑的地方，port 必须是字符串，否则会导致 APISIX 启动报错：invalid discovery kubernetes configuration: object matches none of the required然后重启 APISIX，接着向 APISIX 中添加如下路由：$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/66 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d ' { "methods": ["GET"], "uri": "/kubernetes", "plugins": { "proxy-rewrite" : { "regex_uri": ["/kubernetes", "/"] } }, "upstream": { "type": "roundrobin", "discovery_type": "kubernetes", "service_name": "kubernetes-bootcamp" } }'访问 APISIX 的 /kubernetes 地址验证一下：$ curl http://127.0.0.1:9080/kubernetes不过，如果你的 APISIX 运行在 Kubernetes 集群之外，大概率是访问不通的，因为 APISIX 监听的 Endpoints 地址是 Kubernetes 集群内的 Pod 地址：$ kubectl describe endpoints/kubernetes-bootcamp Name: kubernetes-bootcamp Namespace: default Labels: app=kubernetes-bootcamp Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-03-25T00:31:43Z Subsets: Addresses: 10.1.5.12 NotReadyAddresses: <none> Ports: Name Port Protocol ---- ---- -------- <unset> 8080 TCP Events: <none>所以想使用基于 Kubernetes 的服务发现，最佳做法是将 APISIX 部署在 Kubernetes 中，或者使用 APISIX Ingress，关于 APISIX 集成 Kubernetes 的更多信息，可以参考官方文档 基于 Kubernetes 的服务发现 和官方博客 借助 APISIX Ingress，实现与注册中心的无缝集成。参考集成服务发现注册中心API 网关 Apache APISIX 集成 Eureka 作为服务发现API 网关 Apache APISIX 携手 CoreDNS 打开服务发现新大门Nacos 在 API 网关中的服务发现实践SpringBoot使用Nacos进行服务注册发现与配置管理springcloud(十三)：注册中心 Consul 使用详解Spring Cloud Consul 官方文档Consul 简介和快速入门更多实现自定义服务发现https://apisix.apache.org/zh/docs/apisix/discovery/
GitHub Actions 是 GitHub 于 2018 年 10 月推出的一款 CI/CD 服务。一个标准的 CI/CD 流程通常是一个工作流（workflow），工作流中包含了一个或多个作业（job），而每个作业都是由多个执行步骤（step）组成。GitHub Actions 的创新之处在于它将 CI/CD 中的每个执行步骤划分成一个个原子的操作（action），这些操作可以是编译代码、调用某个接口、执行代码检查或是部署服务等。很显然这些原子操作是可以在不同的 CI/CD 流程中复用的，于是 GitHub 允许开发者将这些操作编写成脚本存在放 GitHub 仓库里，供其他人使用。GitHub 提供了一些 官方的 actions，比如 actions/setup-python 用于初始化 Python 环境，actions/checkout 用于签出某个代码仓库。由于每个 action 都对应一个 GitHub 仓库，所以也可以像下面这样引用 action 的某个分支、某个标签甚至某个提交记录：actions/setup-node@master # 指向一个分支 actions/setup-node@v1.0 # 指向一个标签 actions/setup-node@74bc508 # 指向一个 commit你可以在 GitHub Marketplace 中搜索你想使用的 action，另外，还有一份关于 GitHub Actions 的 awesome 清单 sdras/awesome-actions，也可以找到不少的 action。GitHub Actions 入门示例这一节我们将通过一个最简单的入门示例了解 GitHub Actions 的基本概念。首先我们在 GitHub 上创建一个 demo 项目 aneasystone/github-actions-demo（也可以直接使用已有的项目），然后打开 Actions 选项卡：我们可以在这里手工创建工作流（workflow），也可以直接使用 GitHub Actions 提供的入门工作流，GitHub Actions 提供的工作流大体分为四种类型：Continuous integration - 包含了各种编程语言的编译、打包、测试等流程Deployment - 支持将应用部署到各种不同的云平台Security - 对仓库进行代码规范检查或安全扫描Automation - 一些自动化脚本这些工作流的源码都可以在 actions/starter-workflows 这里找到。GitHub 会自动分析代码并显示出可能适用于你的项目的工作流。由于是示例项目，这里我们直接使用一个最简单的工作流来进行测试，选择 Simple workflow 这个工作流，会在 .github/workflows 目录下创建一个 blank.yml 文件，文件内容如下：# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "main" branch push: branches: [ "main" ] pull_request: branches: [ "main" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 # Runs a single command using the runners shell - name: Run a one-line script run: echo Hello, world! # Runs a set of commands using the runners shell - name: Run a multi-line script run: | echo Add other actions to build, echo test, and deploy your project.这个工作流没有任何用处，只是使用 echo 命令输出一行 Hello, world! 以及其他几行日志而已。然后点击 Start commit 按钮提交文件即可：由于这里我们指定了工作流在 push 的时候触发，所以提交完文件之后，这个工作流应该就开始执行了。重新打开 Actions 选项卡：这里显示了项目中所有的工作流列表，我们可以在一个项目中创建多个工作流。可以看到我们已经成功创建了一个名为 CI 的工作流，并在右侧显示了该工作流的运行情况。点击查看详细信息：这里是工作流包含的所有作业（job）的执行情况，我们这个示例中只使用了一个名为 build 的作业。点击作业，可以查看作业的执行日志：详解 workflow 文件在上一节中，我们通过在 .github/workflows 目录下新建一个 YAML 文件，创建了一个最简单的 GitHub Actions 工作流。这个 YAML 的文件名可以任意，但文件内容必须符合 GitHub Actions 的工作流程语法。下面是一些基本字段的解释。name出现在 GitHub 仓库的 Actions 选项卡中的工作流程名称。如果省略该字段，默认为当前 workflow 的文件名。on指定此工作流程的触发器。GitHub 支持多种触发事件，您可以配置工作流程在 GitHub 上发生特定活动时运行、在预定的时间运行，或者在 GitHub 外部的事件发生时运行。参见 官方文档 了解触发工作流程的所有事件。在示例项目中，我们使用了几个最常用的触发事件。比如当 main 分支有 push 或 pull_request 时触发：on: push: branches: [ "main" ] pull_request: branches: [ "main" ]或者开启手工触发工作流：on: workflow_dispatch:这时会在工作流页面出现一个手工执行的按钮：也可以使用定时任务来触发工作流：on: schedule: - cron: "0 2 * * *"jobs一个工作流可以包含一个或多个作业，这些作业可以顺序执行或并发执行。下面定义了一个 ID 为 build 的作业：jobs: build: ...jobs.<job-id>.runs-on为作业指定运行器（runner），运行器可以使用 GitHub 托管的（GitHub-hosted runners），也可以是 自托管的（self-hosted runners）。GitHub 托管的运行器包括 Windows Server、Ubuntu、macOS 等操作系统，下面的例子将作业配置为在最新版本的 Ubuntu Linux 运行器上运行：runs-on: ubuntu-latestjobs.<job-id>.steps作业中运行的所有步骤，步骤可以是一个 Shell 脚本，也可以是一个操作（action）。在我们的示例中一共包含了三个步骤，第一步使用了一个官方的操作 actions/checkout@v3：# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3这个操作将代码仓库签出到运行器上，这样你就可以对代码运行脚本或其他操作，比如编译、测试或构建打包等。第二步，使用 echo 命令输出一句 Hello, world!：# Runs a single command using the runners shell - name: Run a one-line script run: echo Hello, world!第三步，继续执行多条 echo 命令：# Runs a set of commands using the runners shell - name: Run a multi-line script run: | echo Add other actions to build, echo test, and deploy your project.跟踪 GitHub 趋势项目学习了 GitHub Actions 的基本知识后，我们就可以开始使用它了。除了常见的 CI/CD 任务，如 自动构建和测试，打包和发布，部署 等，还可以使用它来做很多有趣的事情。GitHub 有一个 Trending 页面，可以在这里发现 GitHub 上每天、每周或每月最热门的项目，不过这个页面没有归档功能，无法追溯历史。如果我们能用爬虫每天自动爬取这个页面上的内容，并将结果保存下来，那么查阅起来就更方便了。要实现这个功能，必须满足三个条件：能定时执行：可以使用 on:schedule 定时触发 GitHub Actions 工作流；爬虫脚本：在工作流中可以执行任意的脚本，另外还可以通过 actions 安装各种语言的环境，比如使用 actions/setup-python 安装 Python 环境，使用 Python 来写爬虫最适合不过；能将结果保存下来：GitHub 仓库天生就是一个数据库，可以用来存储数据，我们可以将爬虫爬下来的数据提交并保存到 GitHub 仓库。可以看到，使用 GitHub Actions 完全可以实现这个功能，这个想法的灵感来自 bonfy/github-trending 项目，不过我在这个项目的基础上做了一些改进，比如将每天爬取的结果合并在同一个文件里，并且对重复的结果进行去重。首先我们创建一个仓库 aneasystone/github-trending，然后和之前的示例项目一样，在 .github/workflows 目录下创建一个流水线文件，内容如下：# This workflow will scrap GitHub trending projects daily. name: Daily Github Trending on: schedule: - cron: "0 2 * * *" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run Scraper run: | python scraper.py # Runs a set of commands using the runners shell - name: Push to origin master run: | echo start push git config --global user.name "aneasystone" git config --global user.email "aneasystone@gmail.com" git add -A git commit -m $(date '+%Y-%m-%d') git push在这里我们使用了 on.schedule.cron: "0 2 * * *" 来定时触发工作流，这个 cron 表达式需符合 POSIX cron 语法，可以在 crontab guru 页面上对 cron 表达式进行调试。不过要注意的是，这里的时间为 UTC 时间，所以 0 2 * * * 对应的是北京时间 10 点整。注：在实际运行的时候，我发现工作流并不是每天早上 10 点执行，而是到 11 点才执行，起初我以为是定时任务出现了延迟，但是后来我才意识到，现在正好是夏天，大多数北美洲、欧洲以及部分中东地区都在实施 夏令时，所以他们的时间要比我们早一个小时。工作流的各个步骤是比较清晰的，首先通过 actions/checkout@v2 签出仓库代码，然后使用 actions/setup-python@v2 安装 Python 环境，然后执行 pip install 安装 Python 依赖。环境准备就绪后，执行 python scraper.py，这就是我们的爬虫脚本，它会将 GitHub Trending 页面的内容爬取下来并更新到 README.md 文件中，我们可以根据参数爬取不同编程语言的项目清单：languages = ['', 'java', 'python', 'javascript', 'go', 'c', 'c++', 'c#', 'html', 'css', 'unknown'] for lang in languages: results = scrape_lang(lang) write_markdown(lang, results)数据爬取成功后，我们在工作流的最后通过 git commit & git push 将代码提交到 GitHub 仓库保存下来。你可以在这里 aneasystone/github-trending 查看完整的代码。参考GitHub Actions 文档GitHub Actions 快速入门 - 在 5 分钟或更短的时间内尝试 GitHub Actions 的功能。了解 GitHub Actions - 学习 GitHub Actions 的基础知识，包括核心概念和基本术语。GitHub Actions 的工作流程语法 - 工作流程是可配置的自动化过程，由一个或多个作业组成。 您必须创建 YAML 文件来定义工作流程配置。GitHub Actions 入门教程 - 阮一峰的网络日志更多其他示例结合 GitHub Actions 的自动化功能，我们可以做很多有趣的事情。比如官方文档中还提供了 其他几个示例，用于检测仓库中失效的链接。另外，阮一峰在他的 入门教程 中介绍了一个示例，用于将 React 应用发布到 GitHub Pages。在本地运行 GitHub Actionshttps://github.com/nektos/act
15 年的时候，我曾经写过一个 Typecho 文章二维码的插件，发布在 Typecho 的论坛里，自那以后，由于时间和精力有限（其实是由于懒癌和拖延症晚期），这个插件一直没有更新过。当时写这个插件时是基于 jeromeetienne/jquery-qrcode 实现的，不过由于 jquery-qrcode 依赖于 jquery，如果你的站点上有其他插件也依赖 jquery，很可能会出现版本冲突的情况，譬如如果你用到了 jquery 2.x，而 jquery-qrcode 用的是 jquery 1.9，就会导致插件不可用。在这期间，也有不少用户给我反馈过这个插件在他们的站点上不能用，一直想着重新弄一个，趁着周末重新写了个新版本，这个版本基于 davidshimjs/qrcodejs 实现，它不依赖任何其他库。虽然也可以使用其他手段，譬如 同时兼容 jquery 的多个版本，不过实现起来略嫌麻烦。这个版本发布在 我的 Github 上，欢迎大家试用。下载插件的最新版本 并解压，将 QRCode 目录放到 Typecho 的插件目录 /usr/plugins 下；进入 Typecho 控制台 -- 插件管理，在 禁用的插件 列表中，应该会出现 QRCode 插件，启用该插件；在 启动的插件 列表中找到 QRCode 插件，点击 设置 按钮，可以设置二维码的尺寸等参数；访问你的任意一篇文章，如果一切设置 OK，在文章的最下面，评论的上方，应该可以看到为这篇文章自动生成的二维码；使用手机扫描二维码，检查是否可以在手机上访问这篇文章；如果有任何问题，欢迎给我提交 Issues 或 PR。
有一个月左右的时间都没有写博客了，这是因为这个月整个人的心思全都花在对 CodeIgniter 3.0 文档的翻译工作上。从我的 GitHub 主页上也可以看到从7月12号开始到今天8月4号，连续24天提交一直没有间断过。一、序翻译 CodoIgniter 的文档纯属一个偶然的想法，由于在网上经常会看到有人问 3.0 的中文文档什么时候出？而 CodeIgniter 3.0.0 从5月份发布到现在确实也过去快3个月了，而中文文档在这点上确实有着严重的滞后。正好这段时间比较清闲，打算将之前2.2版本的一个项目升级到3.0，而升级的过程中也遇到了不少的问题，于是就决定将 CodeIgniter 3.0.0 的文档从头到尾仔细看一遍。刚好又在网上看到有人问中文文档的事，于是想着为啥我不自己来试着翻译下呢，就这样一边啃着 3.0.0 的英文文档，一边对照着 CodeIgniter 中国的 2.2.2 版本的中文文档，悄悄的就过了一个月了。经过这一个月的努力，我发现翻译工作确实非常辛苦，在这里不得不对那些从事技术书籍翻译的人表示敬意，也对网络上那些对技术文档提供中文翻译的大牛表示敬意。同时也对中文技术文档的缺少和滞后感到无力，不止是 CodeIgniter，只要你去搜你就会发现无论是 PHP 的文档也好，MySQL 的文档也好，最新的中文文档都要落后几个版本。希望能有更多的人加入到技术文档的翻译工作中来。如果有哪位朋友正在翻译，欢迎联系我，我愿尽薄力。二、问题反馈由于本人能力和精力有限，翻译之中难免犯错，欢迎大家批评指正。你可以通过三种途径来反馈问题：直接在我的博客上留言；在 GitHub 上直接提交 Issues ；或者 Fork 我的项目作出修改然后向我提交 Pull Request 。我会尽快修复文档中的错误。三、构建你自己的文档所有 CodeIgniter 文档的源文件都可以从我的 GitHub 上获取，地址是：https://github.com/aneasystone/ci-doc-chinese。你首先需要使用 git clone 将它获取下来：$ git clone https://github.com/aneasystone/ci-doc-chineseCodeIgniter 的文档是采用 ReStructured Text 格式编写的，所以如果你想自己添加或修改文档的话，可以先熟悉下 ReStructured Text 的语法，如果你对 Markdown 的语法有所了解，相信你能很快上手的。另外，你的电脑上需要安装 Python 和 Sphinx，Sphinx 是一个非常强大的文档生成工具，使用它不仅能生成漂亮美观的 HTML 文档，还可以生成其他的各种格式，包括：PDF，EPUB，LaTeX 等等等等。这里有一篇 IBM 的文章介绍了如何通过 Sphinx 制作文档，你可以简单的看一看。如果你想完整的学习它，这里是有一份 Sphinx 的中文文档，也可以去官网看最新的英文文档。除了 Sphinx ，还需要安装 Sphinx 的扩展 sphinxcontrib-phpdomain 和 CodeIgniter 自带的一个程序 cilexer ，cilexer 实际上是 Pygments 的一个插件，用于文档中的代码高亮。具体的安装步骤中文文档里有详细的说明。一切准备就绪后，你就可以直接使用 make html 来生成 HTML 文档了。当然，你也可以使用 make epub 或 make pdf 来生成其他格式的文档，具体的用法可以看下 Makefile 文件。四、资源如果你不想自己折腾，我已经将编译好的 HTML 文件发布到我自己的网站上了，你可以直接在线查看，你也可以和 CodeIgniter 2.2.2 的中文文档（已升级）以及官方的英文版本对照阅读。另外，文末我也提供了离线版下载，你也可以下载到你的电脑上查看，Enjoy!另外，CI 中国也推出了 3.0 的中文文档，建议以该文档的最新版本为准，可以在上面与其他网友进行讨论！CodeIgniter 3.0.0 中文文档（CI 中国版）CodeIgniter 3.0.0 中文文档（本站备份）CodeIgniter 3.0.0 官方英文文档CodeIgniter 3.0.0 中文文档下载
JSONView是一款非常棒的查看JSON格式数据的Chrome扩展，可以从Chrome的WebStore下载，地址在这里。该扩展一开始是在FireFox中流行起来，支持JSON和JSONP两种格式，并能使用JSONLint对JSON数据格式进行校验。官方效果图如下：尽管这个扩展小巧强大，很Sexy，不过也有些不足。个人感觉最大的不足就是无法处理用户自己输入的JSON数据，由于这个扩展是后台静默运行，所以对于很多第一次使用的人来说可能根本就不知道怎么用，看WebStore上的评论就可以看出这一点，很多用户提问说不知道要在哪里输入JSON，并给了一星。殊不知这扩展并不是这么用的，它是专门用来处理Web浏览器接受到的JSON数据，而不是处理用户自己输入的JSON数据。但是对于Web开发人员来说，很多时候确实不是光看看浏览器返回的JSON数据这么简单，况且Chrome浏览器的Network面板已经可以预览JSON数据了。我们大多数时候需要处理自己手上的一些JSON数据，格式化，格式验证，实时编辑等等。幸好JSONView的作者gildas将这款扩展的源码在GitHub上开源了，我们可以从这里将代码下下来自己添加新的功能。于是我利用周末的时间改造了下JSONView，给它添加了处理用户自定义JSON的功能。本来打算研读JSONView的代码让它无缝支持这个功能，但是后来我发现了GitHub上josdejong开源的jsoneditor项目，完胜我自己写的撇脚代码。于是直接将jsoneditor整合进JSONView，三分钟就完成了这强大的功能。因为JSONView是后台运行的，没有browser_action，所以首先我们在manifest.json文件中添加如下代码（关于manifest.json和browser_action的说明可以参考我另一篇博客《我的第一个Chrome扩展：Search-faster》）： "browser_action": { "default_icon" : "jsonview16.png", "default_title" : "JSONView" },这样我们就可以在浏览器右上侧看到JSONView的图标了，如下：但是这时图标还不能点，我们需要在background.js中添加browser_action的点击处理事件：// click on browser action chrome.browserAction.onClicked.addListener(function(tab) { chrome.tabs.create({ url: chrome.extension.getURL("viewer/index.html") }); });我们直接在background.js的最后一行添加上上面的代码，这样当用户点击图标时直接打开一个新页面viewer/index.html。这个页面是我从jsoneditor上的examples里扒出来的，和JSONView已经无关了。我们将jsoneditor下下来，其中有一个example正是我们需要的：03_switch_mode.html，将关联的文件都拷到viewer目录下就搞定了。最后我们打开Chrome扩展的管理页面：chrome://extensions/，并勾选上“开发者模式”，点击“加载正在开发的扩展程序...”，然后选择JSONView的目录，就可以预览并调试我们编写的扩展了。调试的过程中如果修改了代码，只需要点击“重新加载”即可，非常方便。下面是我们的成果，看上去效果还不错：最后的最后，我们使用Chrome自带的“打包扩展程序...”功能将程序打包成crx文件，你可以点击这里下载。另外，完整的源代码在这里。
最近深深的迷上了Markdown，无论是编写代码说明文档还是记日记写博客，都能用Markdown快速轻松的书写。使用过GitHub的人应该都知道Markdown，Markdown让人沉浸在写作之中，而不用关心排版问题，只需几个简单的标记就能完成文档的自动排版。而且由于它是纯文本，具有极强的兼容性，可以方便的转换为各种格式。于是我开始寻找WordPress上支持Markdown的编辑器，虽然有一些但都不是很好。便又开始寻找支持Markdown的博客系统，在这个开源遍地开花的年代，这样的博客系统还真是多如牛毛，让人眼花缭乱。有系统庞大功能性非常复杂的，也有小巧轻盈扩展性非常完美的。在这么多的博客系统中，最终我选择了Typecho，不仅是因为Typecho的轻量高效稳定简洁的特性，而且这款博客系统完全是由国人团队开发，选择使用Typecho也算是对国内的开源事业做出的一点点支持吧。Typecho仅仅使用了7张数据表和不足400KB的代码，就实现了完整的插件与模板机制，打造了一个和WordPress几无二致的博客系统，而且Typecho和WordPress兼容，很容易从WordPress转到Typecho。不过，虽说如此，但是我在安装Typecho以及转换的过程中还是遇到了不少的问题和挫折，在鼓捣了一周左右的时间后，终于大功告成，仅以此文纪念之。一、安装TypechoTypecho的安装非常方便，按照官方的安装步骤，首先从这里下载最新的稳定版本（写这篇博客时，最新版本为1.0），然后解压并上传到服务器Web目录，然后使用浏览器打开你的博客地址，根据Typecho安装程序的提示一步步的安装即可。当然这是理想情况下的安装步骤，实际上我的安装过程并没有这么顺利，在填完我的配置信息提交之后，出现了下面的错误：安装程序捕捉到以下错误:"". 程序被终止, 请检查您的配置信息.对的你没看错，以下错误是什么错误？是个空字符串！不得不说这真的是一个非常坑爹的错误提示。幸亏Typecho是个开源的博客程序，在install.php文件中搜索字符串“安装程序捕捉到以下错误”，于是便找到了这个异常代码的所在之处（无关代码已省略）：try { $installDb = new Typecho_Db($config['adapter'], $config['prefix']); $installDb->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE); /** 初始化数据库结构 */ // ... $scripts = explode(';', $scripts); foreach ($scripts as $script) { $script = trim($script); if ($script) { $installDb->query($script, Typecho_Db::WRITE); // 这里抛出异常 } } } catch (Typecho_Db_Exception $e) { $success = false; $code = $e->getCode(); if (...) { // ... } else { echo ' ' . _t('安装程序捕捉到以下错误: "%s". 程序被终止, 请检查您的配置信息.',$e->getMessage()) . ' '; } }抛出异常的正是上面注释的那句$installDb->query($script, Typecho_Db::WRITE)，而$installDb是这样定义的：$installDb = new Typecho_Db($config['adapter'], $config['prefix']);Typecho_Db类根据用户选择的数据库适配器进行数据库操作，安装时配置页有两个可供选择：Mysql原生的和Pdo驱动的，如下图的下拉列表所示：而在这里我选择了默认的Mysql原生函数适配器，对应的数据库处理代码在/var/Typecho/Db/Adapter/Mysql.php文件中，看到下面的代码我瞬间明白了为什么会有这么奇怪的错误提示了： /** * 数据库连接函数 * * @param Typecho_Config $config 数据库配置 * @throws Typecho_Db_Exception * @return resource */ public function connect(Typecho_Config $config) { if ($this->_dbLink = @mysql_connect($config->host . (empty($config->port) ? '' : ':' . $config->port), $config->user, $config->password, true)) { if (@mysql_select_db($config->database, $this->_dbLink)) { if ($config->charset) { mysql_query("SET NAMES '{$config->charset}'", $this->_dbLink); } return $this->_dbLink; } } /** 数据库异常 */ throw new Typecho_Db_Adapter_Exception(@mysql_error($this->_dbLink)); }原因就在于上面代码中的@mysql_connect和@mysql_select_db，@符号是php中特有的一种错误控制符（error-control operator），意味着如果这个函数抛出异常将被完全忽略掉，没有errorCode，没有errorMessage。所以上面的错误提示信息使用$e->getMessage()方法获取的是个空字符串。安装失败的原因现在已经定位到了是数据库导致的，接下来就看看为什么数据库配置会失败。这时候我注意到上面图片中红色高亮的提示信息：系统将为您自动匹配 SAE 环境的安装选项其实在安装的过程中我就很纳闷，为什么都没有提示我输入数据库的用户名密码这些配置信息，原来是Typecho检测到了我的系统为SAE环境，直接使用SAE里的数据库配置。而我的环境确实是SAE不假，只不过我使用的是SAE的本地开发环境，而且我从来没有做过SAE的数据库配置。按F12查看页面源码发现数据库的配置确实都是空。于是打开SAE的配置文件sae.conf：DocumentRoot path/to/website http_port 80 https_port 443 redis_port 6379 domain sinaapp mysql_user mysql_pass mysql_host mysql_port proxy_host proxy_port 80 proxy_username proxy_password open_xdebug 0 autoupgrade 1其中mysql_user, mysql_pass, mysql_host, mysql_port分别对应数据库的用户名、密码、主机地址和端口号。于是马上填好这些配置参数，重启SAE，重新安装Typecho，这时这些参数都OK了，但还是差了一个参数dbDatabase，这个值一直是app_: 这个app_其实是SAE_MYSQL_DB的值，于是我们查看SAE的代码，在文件sae/emulation/loadsae.php中找到了它的定义：define('SAE_MYSQL_DB', 'app_'.$_SERVER['HTTP_APPNAME']);原来app_是数据库的前缀，后面还有一个叫做HTTP_APPNAME的$_SERVER变量。从变量名可以推断出这应该是SAE应用的名称，所以可能还需要在SAE中创建应用，但是具体操作我就没深究了，而是采用了一个取巧的方法，直接修改下面的这两行HTML源码： SAE_MYSQL_HOST_M, 'user' => SAE_MYSQL_USER, 'password' => SAE_MYSQL_PASS, 'charset' => 'utf8', 'port' => SAE_MYSQL_PORT, 'database' => 'typecho_db' )"> 然后点击“确认”按钮提交，终于看到了安装成功的页面！二、将WordPress数据导出到Typecho接下来，我便马不停蹄的开始了我的Typecho体验之旅。首先第一件事便是将之前WordPress上的博文导到Typecho上来，好消息是Typecho官方已经提供这样的插件了，我们去这里下载wordpresstotypecho然后遵循步骤解压上传到插件目录并在后台的插件管理里启动该插件。（虽然官网上有提示信息说：仅适用于wordpress2.7，但在我的4.2版本下也转换成功了）就在我兴高采烈的去后台的插件管理里点击“启动插件”链接满心期待着我的WordPress华丽变身Typecho时，一个白底的页面并伴随着一个Fatal error吓到了我：Fatal error: Call to undefined function mb_regex_encoding() in typechovarTypechoCommon.php on line 810WTF！一百只草泥马从眼前飞过有木有！哎，没办法，继续看代码看看哪里出错了吧，谁叫我们是程序猿呢。搜索函数mb_regex_encoding，还好，只有Common.php中用到了一次，代码如下：/** * 生成缩略名 */ public static function slugName($str, $default = NULL, $maxLength = 128) { // ... if (__TYPECHO_MB_SUPPORTED__) { mb_regex_encoding(self::$charset); mb_ereg_search_init($str, "[\w" . preg_quote('_-') . "]+"); $result = mb_ereg_search(); // ... } else if ('UTF-8' == strtoupper(self::$charset)) { // ... } else { // ... } $str = trim($str, '-_'); $str = !strlen($str) ? $default : $str; return substr($str, 0, $maxLength); }这段代码的用途是根据字符串自动生成缩略名（slugName），其中使用到的mb_打头的函数都是php提供的针对多字节字符串处理的函数。但是mb_函数并不是所有的php版本都支持，所以在使用时需要先确定系统是否支持，Typecho通过下面的方法来判断系统是否支持mb_函数的：define('__TYPECHO_MB_SUPPORTED__', function_exists('mb_get_info'));可见，Typecho认为只要系统支持mb_get_info函数，应该也支持mb_regex_encoding这些mb_函数。而事实却并不是这样，我尝试了几个不同版本的Windows上的PHP，发现都是支持mb_get_info而不支持mb_regex_encoding，我不知道是不是Windows版本的PHP都会存在这个问题，Google之并没有得到我需要的答案，有人说可能是权限问题，有人说是配置问题，还有人说需要使用--enable-mbstring参数重新编译下PHP。哦，在Windows下重新编译PHP？实在是太酷了！但是，等一下，我好像只是想装个博客系统而已。让我们回到现实，不要走的太远。slugName这个函数也不是什么核心功能，而且我们看代码可以知道，如果不支持mb_函数后面照样能走通，既然如此，我们暂且就认为我们的系统不支持mb_函数好了，这又能怎么样呢？于是稍微改了下代码：define('__TYPECHO_MB_SUPPORTED__', function_exists('mb_regex_encoding'));再点“启用插件”链接，OK，顺利进来了。按照页面提示，需要填写WordPress的数据库配置信息，然后进入“从Wordpress导入数据”页面，点击“开始数据转换”按钮。轻松几秒，转换完成，去“管理文章”页面看下，是不是非常熟悉的文章列表页面又回来了？三、折腾Markdown好了，现在博客已经搭建完毕，可以在Typecho下尽情的使用我最喜爱的Markdown语法进行写作了，这就结束了吗？哦，当然不，我们的Typecho之旅才刚刚开始！谁让我们程序猿都是爱折腾的人呢！我们随便编辑一篇文章，Typecho会弹出下面的提示文字：这篇文章不是由Markdown语法创建的, 继续使用Markdown编辑它吗?选择“是”即可使用Markdown语法书写，这样文章的格式就是Html和Markdown混合格式。这多多少少有些让人感觉不爽，特别是看到类似下面的列表代码时： blah ... blah blah ... blah blah blah ... 对于我们这些有着“洁癖”和“强迫症”的程序猿来说，是不是就有种强烈的冲动想把它改成下面的Markdown代码：- blah ... - blah blah ... - blah blah blah ...怎么办呢？难道我们要手动把之前的所有博客都转换成Markdown语法么？Of course, No. 我们程序猿的特点就是这样：就算是能够在一个小时内手工完成的工作，也要写上六个小时的代码，然后再调试六个小时，最后花三秒钟运行自动完成！Google上搜索 “Html to Markdown” 得到大量结果，说明这种工作已经有大把大把的人做过了，我们要做的就是找个靠谱点的工具直接用就好了。最终我把目光锁定在了html2text这个开源的Python项目上，并且打算写段Python脚本自动将我之前的所有的博客转换为Markdown语法。代码非常简单：def main(): posts = get_all_posts() posts = convert_posts_to_markdown(posts) save_posts(posts) returnget_all_posts函数从MySQL数据库中获取Typecho所有文章列表（也就是typecho_contents表），convert_posts_to_markdown直接调用函数html2text.html2text()将文章转换为Markdown语法，然后再调用save_posts将转换后的文章保存到数据库。理论上这短短三行代码就能瞬间搞定，嗯，理论上。事实上，在我的很多文章中有很多的代码块，而我为了让我的代码块看起来好看一点，使用了WordPress上提供的一个代码高亮插件：SyntaxHighlighter Evolved。它的语法类似于下面这样：[code lang='javascript'] function sayHello() { console.log('hello'); } [/code]在使用html2text.html2text()函数转换这样的代码时，会把它视为普通的段落文本，这样我所有的代码里的空格换行全被剔除了，变成了一团乱麻。于是稍微修改了下程序的逻辑，在转换之前先用正则将文章中所有[code...]...[/code]这种格式的代码替换成唯一的uuid，然后再转换之后再将uuid替换为类似下面的代码：function sayHello() {console.log('hello');}完整的脚本可以参考这里，你可能需要根据你的插件或其他的一些不同的语法做些调整。四、折腾主题样式现在所有的文章都已经转换成Markdown语法了，去主页上随便逛逛，看着自己的劳动成果即将面世还是有些成就感的。接下来便是换个靠谱的主题了。我个人是比较推崇单列布局的宽屏主题，这样不仅显得页面很干净清爽，让人看文章时能保持专注不至于被侧边栏的内容分心，而且在宽屏下代码可以完整的显示出来，如果页面很窄的话，代码过长就会导致出现大量横向的滚动条，如果设置成不显示滚动条又会导致代码出现大量的自动换行，看起来非常不舒服，完全没有在IDE中看代码的感觉。使用WordPress时我使用的就是这样的主题：Decode，而且自己做了点调整，将内容的宽度调大了一点。于是，我还要找一个类似这样的Typecho主题。最终，我选择了Typecho论坛上一个非常火的主题：Navy，虽然没有找到这个主题比较官方的链接，但是可以直接从论坛里下载使用。Navy主题清爽简洁，唯一不足的一点是：右侧栏展开后只能显示“最新文章”和部分的“分类”，下面剩下的“分类”部分和“归档”都未显示，并且也不能用鼠标滚轮向下滚动。于是修改了下样式文件navy/style.css，对侧边栏增加了overflow-y: auto;：#secondary { height: 100%; width: 300px; top: 0; right: -330px; position: fixed; z-index: 100; box-shadow: -3px 0 8px rgba(0, 0, 0, 0.1); font-size: 13px; background-color: #303538; color: #f8f8f8; overflow-y: auto; /* 右侧栏显示垂直滚动条 */ }这样修改之后，侧边栏右边会出现一个滚动条，这个滚动条和浏览器的滚动条并行显示，虽然看起来比较ugly，但也勉强够用了。另外对Navy主题的样式还做了些其他的定制（不感兴趣的同学请直接忽略）：.container { width:1150px; /* 增大内容宽度 */ padding:0 60px; margin:0 auto; } a, button.submit { color:#009BCD; /* 链接颜色改为淡蓝色 */ text-decoration:none; -webkit-transition:all .1s ease-in; -moz-transition:all .1s ease-in; -o-transition:all .1s ease-in; transition:all .1s ease-in; } pre, code { background: #F0F0EC; color: #C13; /* 代码的背景为灰色，字体为暗红色 */ font-family:Menlo, Monaco, Consolas, "Lucida Console", "Courier New", monospace; font-size:.92857em; /*display: block;*/ /* 代码块内联显示 */ } .post-content img { max-width:100%; /* 防止图片太宽撑出去 */ margin-left: 0; } blockquote,.stressed { -moz-box-sizing: border-box; box-sizing: border-box; margin: 1.8em 0 1.8em 2.2em; /* 引用文字改为向内缩进，太突出感觉怪怪的 */ padding: 0 3.8em 0 1.6em; border-left: #4a4a4a 0.4em solid; /* font-style: italic; */ /* 去掉引用文字的斜体 */ color: #888; } .post-content ul { overflow: auto; /*padding: .5em 2.4em;*/ border-radius: 3px; /*margin:1.8em 0;*/ /* 列表的内外边距都太大了 */ }五、折腾代码高亮到这里，博客基本上已经打造的差不多了，但是还差最后一步，Typecho默认的代码样式简直让人想起了上个世纪在记事本里写代码的年代。确实，在如今的开发环境中，代码高亮几乎已成为标配，没有高亮的代码无论是给人阅读，还是书写都让人感到非常不爽。所以，最后一步，我来为我的Typecho加上代码高亮的功能，而这样的插件也直接就已经有现成的了，我这里还是使用著名的SyntaxHighlighter插件来实现代码高亮，它的Typecho版是：SyntaxHighlighter-For-Typecho。从GitHub上直接"Download ZIP"，然后解压并上传到服务器上的Typecho插件目录（要特别注意的是：插件的目录名和插件类名必须保持一致，譬如这里的类名为SyntaxHighlighter_Plugin，所以目录名必须是SyntaxHighlighter，要不然会出现500 Server Error，这又是一个非常非常坑爹的错误提示！我认为Typecho在出错时的提示信息上面还可以做的更好）六、Markdown标记小结Typecho实现了最基本的一些Markdown语法，我这里总结下。6.1 字体加粗语法：**粗体**结果：粗体6.2 斜体语法：*斜体*结果：斜体6.3 链接语法：[百度](http://www.baidu.com)结果：百度6.4 图片语法：![image text](https://www.baidu.com/img/bdlogo.png)结果：6.5 标题语法：# This is ## This is ### This is 结果：This is This is This is 6.6 列表语法：1. 有序列表One2. 有序列表Two3. 有序列表Three- 无序列表One- 无序列表Two- 无序列表Three结果：有序列表One有序列表Two有序列表Three无序列表One无序列表Two无序列表Three6.7 引用语法：> 这是引用文字结果：这是引用文字6.8 行内代码语法： `var i = 0;` 结果：var i = 0;6.9 代码块语法： ``` function sayHello() {} ``` 结果：function sayHello() { }Typecho支持的Markdown语法有限，其实，还有很多其他的语法在特定的场景下也会比较有用，譬如绘制表格，删除线，表情，LaTeX格式，流程图，序列图等等。可以看下作业部落的Cmd Markdown编辑器，其华丽程度简直让人目瞪口呆。然后这里有一份比较规矩还不错的语法说明，另外GitHub也有Markdown的一份指导文档。参考开始用 Markdown 写博客Typecho文档站点php - @mysql_connect and mysql_connect - Stack OverflowPHP手册 > 函数参考 > 国际化与字符编码支持 > 多字节字符串Cmd Markdown 编辑阅读器 - 作业部落出品Markdown 语法说明 (简体中文版)Mastering Markdown · GitHub Guides
花了两周左右的时间将Chrome扩展的开发文档看了一遍，把所有官方的例子也都顺便一个个的安装玩了一遍，真心感觉Chrome浏览器的博大精深。Chrome浏览器的现有功能已经足够强大，再配合Chrome扩展几乎可以说是“只有想不到，没有做不到”。于是利用业余时间做了一个简单的扩展Search-faster，可以加快Google的搜索速度，算是对近一段时间学习的总结。一、Chrome扩展综述Chrome扩展有两种不同的表现形式：扩展（Extension）和应用（WebApp），我们这里不讨论WebApp，但是扩展的大多数技巧对于WebApp来说也是适用的。Chrome扩展实际上就是压缩在一起的一组文件，包括HTML，CSS，Javascript，图片，还有其它任何需要的文件。它从本质上来说就是一个Web页面，可以使用所有的浏览器提供的API，可以与Web页面交互，或者通过content script或cross-origin XMLHttpRequests与服务器交互。还可以访问浏览器提供的内部功能，例如标签或书签等。扩展在Chrome浏览器中又有着两种不同的表现形式：browser_action和page_action，browser_action在工具栏右侧添加一个图标，page_action在URL输入栏右侧添加一个图标，如下图所示。这两个action唯一的区别在于：当你的扩展是否显示取决于单个页面时，该使用page_action，page_action默认是不显示的。1.1 manifest.json每一个Chrome扩展都有一个清单文件包含了这个扩展的所有重要信息，这个文件的名称固定为manifest.json，文件内容为JSON格式。下面是一个manifest.json文件的实例（来自JSONView扩展）{ "background": { "scripts": [ "background.js" ] }, "content_scripts": [ { "all_frames": true, "js": [ "content.js" ], "matches": [ "http://*/*", "https://*/*", "ftp://*/*", "file:///*" ], "run_at": "document_end" } ], "description": "Validate and view JSON documents", "icons": { "128": "jsonview128.png", "16": "jsonview16.png", "48": "jsonview48.png" }, "key": "...", "manifest_version": 2, "name": "JSONView", "options_page": "options.html", "permissions": [ "clipboardWrite", "http://*/", "contextMenus", "https://*/", "ftp://*/" ], "update_url": "https://clients2.google.com/service/update2/crx", "version": "0.0.32.2", "web_accessible_resources": [ "jsonview.css", "jsonview-core.css", "content_error.css", "options.png", "close_icon.gif", "error.gif" ] }其中name、version和manifest_version三个字段是必选的，每个字段的含义显而易见，另外在当前版本下manifest_version的值推荐为2，版本1已经被弃用。除这三个字段之外，description为对扩展的一句描述，虽然是可选的，但是建议使用。icons为扩展的图标，一般情况下需要提供三种不同尺寸的图标：16*16的图标用于扩展的favicon，在查看扩展的option页面时可以看到；48*48的图标在扩展的管理页面可以看到；128*128的图标用于WebApp。这三种图标分别如下所示：图标建议都使用png格式，因为png对透明的支持最好。要注意的是：icons里的图标和browser_action或page_action里的default_icon可能是不一样的，default_icon显示在工具栏或URL输入栏右侧，建议采用19*19的图标。key字段为扩展的唯一标识，这个字段是浏览器在安装.crx文件时自动生成的，通常不需要手工指定。permissions为扩展所需要的权限列表，列表中的每一项要么是一个已知的权限名称，要么是一个URL匹配模式。一些常见的权限名称有background、bookmarks、contextMenus、cookies、experimental、geolocation、history、idle、management、notifications、tabs、unlimitedStorage等；URL匹配模式用于指定访问特定的主机权限，譬如："http://*.google.com/"、"http://www.baidu.com/"。关于permissions字段可以参考这里的文档。update_url用于扩展的自动升级，默认情况下Chrome浏览器会每隔一小时检测一次是否需要升级，也可以点击扩展管理页面的“立即更新”按钮强制升级。另外background、content_scripts、options_page这三字段，还有这个例子里没包含的browser_action/page_action字段是构成Chrome扩展的核心元素。下面分别进行介绍。1.2 background背景页通常是Javascript脚本，是一个在扩展进程中一直保持运行的页面。它在你的扩展的整个生命周期都存在，在同一时间只有一个实例处于活动状态。在manifest.json中像下面这样使用scripts字段注册背景页：{ "name": "My extension", // ... "background": { "scripts": ["background.js"] }, // ... }也可以使用page字段注册HTML页面：{ "name": "My extension", // ... "background": { "page": ["background.html"] }, // ... }背景页和browser_action/page_action是运行在同一个环境下的，可以通过chrome.extension.getBackgroundPage()和chrome.extension.getViews()进行两者之间的互相通信。背景页也常常需要和content_scripts之间进行通信，要特别注意的是背景页和content_scripts是运行在两个独立的上下文环境中的，只能通过messages机制来通信，这个通信可以是双向的，首先写下消息的监听方：chrome.extension.onRequest.addListener(function(request, sender, callback) { console.log(JSON.stringify(request)); // deal with the request... sendResponse({success: true}); });然后写下消息的发送方：chrome.tabs.sendRequest(tabId, cron, function(response) { if (response.success) { // deal with the response... } });1.3 content_scriptscontent scripts是一个很酷的东西，它可以让我们在Web页面上运行我们自定义的Javascript脚本。content scripts可以访问或操作Web页面上的DOM元素，从而实现和Web页面的交互。但是要注意的是，它不能访问Web页面中的Javascript变量或函数，content scripts是运行在一个独立的上下文环境中的，类似于沙盒技术，这样不仅可以确保安全性，而且不会导致页面上的脚本冲突（譬如Web页面上使用了jquery 1.9版本，而content scripts中使用了jquery 2.0版本，这两个版本的jquery其实运行在两个独立的上下文环境中互不影响）。content scripts除了不能访问Web页面中Javascript变量和函数外，还有其他的一些限制：不能使用除了chrome.extension之外的chrome.* 的接口不能访问它所在扩展中定义的函数和变量不能做cross-site XMLHttpRequests但这些限制其实并不影响content scripts实现其强大功能，因为可以使用Chrome扩展的messages机制来和其所在的扩展进行通信，从而间接的实现上面的功能；而且，content scripts甚至可以通过操作DOM来间接的和Web页面进行通信。使用content scripts在Web页面注入自定义脚本可以通过两种方法来实现：第一种方法是在manifest.json文件中使用content_scripts字段来指定，还有一种方法是通过编程的方式调用chrome.tabs.executeScript()函数动态的注入。这里有详细的介绍。1.4 options_page当你的扩展拥有众多参数可供用户选择时，可以通过选项页来实现。选项页就是一个单纯的HTML文件，可以引用脚本，CSS，图片等其他资源。这在Web开发中是家常便饭，只要你会制作网页，那么制作一个选项页肯定也没问题，这并没有什么好说的。但是，如果我们仔细想一想，当用户在选项页点击保存修改后，修改后的配置信息保存在哪儿呢？如何做到选项页中的配置在重启浏览器后甚至是清除浏览器数据后仍然存在呢？这就需要我们将配置信息保存到硬盘上的某个文件中，而浏览器Web脚本中的Javascript代码很显然是不能访问物理文件的。这就是chrome.storage.local的由来，chrome.storage.local是Chrome浏览器提供的存储API，这个接口用来将扩展中需要保存的数据写入本地磁盘。Chrome提供的存储API可以说是对localStorage的改进，它与localStorage相比有以下区别：如果储存区域指定为sync，数据可以自动同步在隐身模式下仍然可以读出之前存储的数据读写速度更快用户数据可以以对象的类型保存清除浏览器数据后仍然可以访问1.5 browser_action vs. page_action上面已经说过，browser_action和page_action是扩展在Chrome浏览器中的两种不同的表现形式，browser_action显示在工具栏右侧，page_action显示在URL输入栏右侧。下面的代码示例说明了如何注册一个browser_action（page_action的注册方法类似，只要将browser_action替换成page_action即可）：{ "name": "My extension", // ... "browser_action": { "default_icon": "images/icon19.png", // optional "default_title": "Google Mail", // optional; shown in tooltip "default_popup": "popup.html" // optional }, // ... }一个browser_action可以拥有一个icon，一个tooltip，一个badge和一个popup，page_action没有badge，也可以拥有一个icon，一个tooltip和一个popup。icon是action的图标，一般情况下是一个19*19的png图片，也可以是HTML5中的一个canvas元素可以实现任意的自定义图片；tooltip是提示信息，当鼠标移到action图标上时会显示出来；popup是当用户点击action图标时弹出的窗口；badge是写在图标上文字，譬如下图中显示在RSS Feed Reader这个扩展图标上的67就是一个badge，由于badge空间有限，一般不会超过4个字符，超出部分会被截断。如果在default_popup字段中指定了popup.html，当用户点击图标时就会弹出来。这也是个简单的HTML文件，可以包含自己的脚本，样式和图片文件。如果没有指定popup.html，点击图标时会触发action的onClicked事件。如果你需要处理该事件可以在背景页background.js中使用类似于下面的代码：chrome.browserAction.onClicked.addListener(function(tab) { // ... });browser_action默认总是显示，除非你在扩展管理里选择了隐藏按钮，而page_action默认是不显示的，需要使用函数chrome.pageAction.show()和chrome.pageAction.hide()来控制page_action的显示。下面是一个简单的示例，只有当URL是www.baidu.com才会显示page_action：function update(tabId) { if (location.host.indexOf('www.baidu.com') == -1) { chrome.pageAction.hide(tabId); } else { chrome.pageAction.show(tabId); } } chrome.tabs.onUpdated.addListener(function(tabId, change, tab) { if (change.status == "complete") { update(tabId); } }); chrome.tabs.onSelectionChanged.addListener(function(tabId, info) { update(tabId); }); chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { update(tabs[0].id); });二、如何调试Chrome扩展调试永远是软件开发中至关重要的一步，无论是桌面应用还是Web应用都是如此。Chrome浏览器的开发者工具提供给开发人员一个近乎完美的Web调试器，不仅可以查看Web页面的HTML源码，分析和修改DOM树，调试CSS样式，查看每一个网络请求进行性能分析，以及强大的Javascript脚本调试器。Chrome扩展和Web应用别无二致，自然也可以使用相同的调试方法对其进行调试，只不过要在几点不同之处特别注意一下。2.1 popup的调试如果你的扩展使用了popup，无论是browser_action还是page_action，都可以通过下面的方法调试popup：首先在扩展的图标上点击右键，然后选择“审查弹出内容”，如下图所示：这时会弹出开发者工具的窗口，我们选择Sources选项卡，在左侧就可以看到所有popup相关的HTML、Javascript以及CSS了。如下图：查看Javascript代码找到我们感兴趣的地方，在该处下个断点，然后就可以进行调试了。如果断点处的代码是弹出窗口时就已经执行过了，那么可以切换到Console选项卡，输入location.reload(true)执行后popup会重新加载，并断在断点处，如下图：2.2 background的调试尽管背景页和popup是属于同一个执行环境下，但是点击“审查弹出内容”时并不能看到背景页的代码，也不能对其进行调试。要调试背景页，首先需要打开扩展管理页面，找到要调试的扩展，如果该扩展有背景页，会显示类似于如下图所示的“检查视图：background.html”字样，用户点击background.html弹出开发者工具既可以进行调试。和popup一样，也可以使用在Console选项卡中执行location.reload(true)这个小技巧来重新加载背景页。2.3 content_scripts的调试content_scripts是注入到Web页面中的Javascript代码，所以调试content_scripts和调试Web页面是完全一样的。我们直接按F12调出开发者工具，然后切换到Sources选项卡，在下面的左侧可以看到又有几个小的选项卡：Sources、Content scripts、Snippets。我们选择Content scripts就可以找到已经注入到这个页面的所有content_scripts。如下图：可以看出一个页面可以被注入多个content_scripts，每一个content_scripts都有着他们自己独立的运行空间。找到感兴趣的代码下断点，然后就可以调试了。如果代码已经运行过，刷新下页面即可（当然，如果你在Console选项卡中执行location.reload(true)也是完全可以的，但这哪有F5方便呢:-)）。2.4 option.html的调试选项页就是一个静态的HTML页面，和调试Web页面完全一样。没什么好说的了。三、Search-faster的实现通过上面的学习，我们基本上已经了解到了开发一个Chrome扩展所需要的基本知识了。现在我们通过实现一个最最简单的Chrome扩展来对学到的内容进行巩固。Search-faster非常简单，写它的目的也非常简单：加快我们在搜索引擎上的搜索速度。听起来很高大上，其实很简单，我们知道在我们搜索的时候，很多搜索引擎搜出来的结果并不是直接跳转到原网页，而且先跳转到搜索引擎自身，然后再跳转到原网页。如下图所示，百度搜索就是这样做的：当然Google也是这样：这样做搜索引擎可以对每个搜索结果进行统计分析然后优化，但是对于我们用户来说，多做一次跳转显然会降低我们的速度。而且在我们大天朝，通过代理访问Google本来就已经够慢的了，点击每个搜索结果再跳转一次到Google实在是有点让人受不了。我本来打算对百度、Google和Bing做统一处理的，后来发现百度的跳转链接是经过加密后的，一时破解不了，而Bing的搜索结果并没有跳转而是直接到原网页。于是这个Search-faster其实就变成了Google-search-faster了。首先我们确定我们的扩展类型，因为只有在访问Google搜索时才需要显示，所以我们采用page_action而不是browser_action。然后我们确定需要哪些文件，因为要访问Google搜索的结果页面，所以肯定需要一个content_scripts，content_scripts的内容是遍历Google搜索结果页面上的所有跳转链接，获取每个链接的原链接，然后在每个链接上添加一个click事件，当用户点击该链接时直接跳转到原链接。（本来是打算直接修改链接为原链接的，但是发现Google的代码中有检测功能，会自动将跳转链接替换回来，所以使用click事件的方法最为保险）。最后我们需要一个背景页，检测浏览器选项卡的变动，当用户切换选项卡或选项卡有变动时执行content_scripts。manifest.json的代码如下：{ "name": "Search-faster", "version": "1.0", "description": "Replace the search engine redirect url to direct url when searching baidu, google, etc.", "background": { "scripts": ["background.js"] }, "content_scripts": [{ "matches": ["http://*/*", "https://*/*"], "js": ["jquery.min.js", "content_script.js"] }], "page_action": { "default_icon" : "icons/google.png", "default_title" : "It works!" }, "permissions" : ["tabs"], "manifest_version": 2 }content_scripts的关键代码如下：chrome.extension.onRequest.addListener(function(req, sender, sendResponse) { var response = doReplace(); sendResponse(response); }); /** * replace the redirect url to direct url */ function doReplace() { // url not match if(location.host.indexOf('www.google.com') == -1) { return null; } // get the keyword var lstib = $('#lst-ib'); if(lstib.length == 0) { return null; } // get the links & add a click event var links = $('.srg .g h3.r a'); links.on('click', function(e) { var href = $(e.target).attr('data-href'); window.open(href); return false; }); return { keyword: lstib[0].value, replace_cnt: links.length }; }background的关键代码如下：function updateSearch(tabId) { chrome.tabs.sendRequest(tabId, {}, function(search) { searches[tabId] = search; if (!search) { chrome.pageAction.hide(tabId); } else { chrome.pageAction.show(tabId); if (selectedId == tabId) { updateSelected(tabId); } } }); } chrome.tabs.onUpdated.addListener(function(tabId, change, tab) { if (change.status == "complete") { updateSearch(tabId); } }); chrome.tabs.onSelectionChanged.addListener(function(tabId, info) { selectedId = tabId; updateSelected(tabId); }); chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { updateSearch(tabs[0].id); });完整的代码在这里。参考综述--扩展开发文档Chrome 扩展程序、应用开发文档（非官方中文版）Sample Extensions - Google ChromeChrome插件（Extensions）开发攻略手把手教你开发chrome扩展一：开发Chrome Extenstion其实很简单手把手教你开发Chrome扩展二：为html添加行为手把手教你开发Chrome扩展三：关于本地存储数据Chrome.storage和HTML5中localStorage的差异