文档生成是开发人员生活中非常普遍的需求。无论是电子商务网站、管理应用程序还是其他任何东西。它可以是发票生成、保险文件准备、医生处方、人力资源报价生成、工资单生成,你可以想到大量的用例。总是需要生成文档。
从开发人员的角度来看,有几种常见的方法可以完成这项工作。
这些方法对我没有帮助。客户希望自己定制他们的文件。我一直在寻找一种方法,发现eDocGen是一种单点解决方案。
与其他服务不同,eDocGen 提供了可以集成到我们应用程序中的 RestAPI。
在本文中,我们将讨论如何将 eDocGen 集成到我们的 js 应用程序中,以从各种数据格式(如 JSON/XML/Database 模式)生成文档。请免费试用以开始编码。
让我们潜入并编写代码。
出于演示目的,我创建了一个在 nodejs 上运行的示例 js 应用程序。
请按照以下步骤为我们设置编码游乐场。
用于npm init创建 package.json
添加axios, form-data, request,xhr2开发此应用程序所需的依赖项npm install axios form-data request xhr2
我们需要一个索引文件作为我们应用程序的起点。在根目录中创建一个 index.js 文件并修改 package.json 如下所示。
JSON
scripts": {
"start": "node index.js"
}
现在我们有一个基本的应用程序可以开始。这些步骤结束后,package.json 应该如下所示。
JSON
{
"name": "nodejs-multiple-upload-files",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"axios": "^0.27.2",
"form-data": "^4.0.0",
"request": "^2.88.2",
"xhr2": "^0.2.1"
}
}
虽然这篇文章是关于文档生成的,但我们需要登录才能获取我们的访问令牌。这是一个典型的JWT令牌,将用于授权文档生成 API。
JavaScript
var XMLHttpRequest = require("xhr2");
var xhr = new XMLHttpRequest();
module.exports.getToken = function (callback) {
var data = JSON.stringify({
username: "<your username>",
password: "<password>",
});
xhr.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
token = JSON.parse(this.responseText).token;
console.log("User Token", token);
callback(token);
}
});
xhr.open("POST", "https://app.edocgen.com/login");
xhr.setRequestHeader("content-type", "application/json");
xhr.setRequestHeader("cache-control", "no-cache");
xhr.send(data);
};
我们可以将令牌在应用程序中缓存一个小于过期时间的时间段,并使用它来生成文档或上传模板。到期时间过后,我们可以刷新令牌。缓存可以是 Redis 或内存缓存。这取决于您的应用程序设计。
如上所述,eDocGen 允许用户自定义和上传模板。但是如何动态映射数据呢?有一些将数据映射到文档的规则。我们将看到如何使用规则创建模板。
看看这个文件。
eDocGen{}对动态字段使用由 括起来的标签。我们可以动态添加文字、logo、表格、条件语句、数学计算等。
例如,在上图中,
字符串字段: {Invoice_Number}并{Invoice_Date}配置为替换为模板中的文本。模板中 {} 内的任何内容都将与输入数据匹配并替换。
动态表: 当表中存在需要循环和替换的数据数组时,动态表将是一个不错的选择。表中的行以 开头{#tablename}和结尾{/tablename}。在上面的示例中,发票表中的一行在第一列以 {#IT} 开头,在最后一列以 {/IT} 结尾。行中的列可以有字符串字段。在我们的示例中,{Item_description}并且{Amount}
图片: eDocGen 提供动态添加图片到模板的功能。请按照以下步骤操作。
基于条件的动态字段(If-Else):可以使用条件标签有条件地显示内容。例如,当语言为英语时,文档中会显示{#language == "english"} 英语内容。同样,单个文档模板可以支持多种语言。
数学计算: eDocGen 支持基于模板中定义的公式的数学计算。可以使用以下公式计算发票中项目金额的总和。
JSON
{
IT // array of items
| summation:'Amount' // value that needs to be used for calculation
| format_number: ",” // format of the value
}
请前往JSON-to-pdf了解更多详情。
准备好模板后,就可以将其上传以供使用。有两种方法。
对于演示,我使用 UI 来上传模板。成功上传后,我们会得到一个 ID 作为响应。这是将用于生成文档的 ID。
如果您希望使用 API,请在此处留下 Upload API 结构供您参考。
JSON
"/api/v1/document": {
"post": {
"tags": [
"Document"
],
"description": "Upload template to eDocGen",
"produces": [
"application/json"
],
"consumes": [
"multipart/form-data"
],
"parameters": [
{
"name": "documentFile",
"description": "file to upload",
"required": true,
"type": "file",
"in": "formData"
},
{
"name": "x-access-token",
"in": "header",
"description": "JWT auth token from login",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "Successfully uploaded document file"
},
"other": {
"description": "Operation failed"
}
}
}
}
现在我们准备好了模板。让我们生成文档。
文档生成有两个阶段。
我们要求生成包含所需详细信息的文档,并得到确认。该过程异步发生在屏幕后面。
应用程序接口:POST-/api/v1/document/generate/bulk
表格数据
文档 ID | 模板的id |
格式 | pdf/docx(模板应支持格式) |
输出文件名 | 输出文件的文件名。 |
输入文件 | 该文件包含标记值。支持 json、xlsx 和 xml。 |
内容类型 | 多部分/表单数据 |
x-访问令牌 | 来自登录的 JWT 身份验证令牌 |
inputFile 中的数据应该是模板定义的结构。例如,对于上面的模板映射将如下所示。
可以使用从上述步骤中获得的输出 ID 和输出文件的名称下载生成的文档。
我们将在这里使用两个 API。
由于文档生成是异步发生的,要知道文档是否生成,我们将使用/api/v1/output/nameapi。
来自 API 的成功响应/api/v1/output/name将下载文件。
我将这两个步骤组合在一个 js 文件中,如下所示。
爪哇
let login = require("../edocgen_login");
const fs = require("fs");
const uuid = require("uuid");
const FormData = require("form-data");
let axios = require("axios");
let fileName = uuid.v4();
const headers = {
"Content-Type": "multipart/form-data",
"x-access-token": "null",
};
const hostName = "https://app.edocgen.com/api/v1/document/generate/bulk";
const outputFormat = "<format>";// pdf / docx
const documentId = "<template_id>"; // id of the template we want to use
module.exports.generateFiles = function () {
let authToken = login.getToken(function handleUsersList(token) {
headers["x-access-token"] = token;
var formBody = new FormData();
formBody.append("documentId", documentId);
formBody.append("format", outputFormat);
formBody.append("outputFileName", fileName);
// json data for the template
formBody.append("inputFile", fs.createReadStream("./JSON_Data_Single.json")); // local path forjson file
let config = {
method: "post",
url: hostName,
headers: headers,
data: formBody,
};
console.log(`https://app.edocgen.com/api/v1/output/name/${fileName}.${outputFormat}`);
let config_output = {
method: "get",
url:`https://app.edocgen.com/api/v1/output/name/${fileName}.${outputFormat}`,
headers: headers,
};
const MAX_RETRY = 50;
let currentRetry = 0;
// max retry for 50 times
function errorHandler() {
if (currentRetry < MAX_RETRY) {
currentRetry++;
console.log("Document is not prepared yet! Retrying...");
sendWithRetry(processResponse);
} else {
console.log("No luck. Document is not generated. Retried multiple times.");
}
}
// sendWithRetry checks for file existence
// on success, it proceeds to download the file
// on failure, it retries
// todo: introduce spin lock
function sendWithRetry(callback) {
axios(config_output)
.then(function (response) {
if (response.data.output.length !== 1) {
throw new axios.Cancel("Document is not found. Throw error.");
} else {
callback(response);
}
})
.catch(errorHandler);
}
axios(config)
.then(function (response) {
sendWithRetry(processResponse);
})
.catch(function (error) {
console.log(error);
});
});
};
function processResponse(response) {
const outputId = response.data.output[0]._id;
console.log(
"Output Document is Generated. Id = ",
response.data.output[0]._id
);
let config_download = {
method: "get",
url: `https://app.edocgen.com/api/v1/output/download/${outputId}`,
headers: headers,
responseType: "arraybuffer",
};
axios(config_download)
.then(function (response) {
console.log("Output file is downloaded " + `${fileName}.${outputFormat}`);
fs.writeFileSync(`./${fileName}.${outputFormat}`, response.data);
})
.catch(function (error) {
console.log("Error while downloading");
console.log(error);
});
}
当数据为单个 JSON 时,将生成给定格式的单个文档。
当数据是对象数组时,将生成每个数组元素的文档并将其压缩到文件中。
XML 数据的过程很简单。我们需要做的就是传递 XML 文件来代替 JSON 数据。
就像JSON to document,XML to Document 我们也需要documentId, outputFileName, format and inputFile。除输入文件外,与 JSON 相同的所有内容都将是 XML 文件。
示例 XML 数据如下所示
XML
<?xml version="1.0" encoding="UTF-8" ?>
<marker>
<values>
<Invoice_Number>SBU-2053501</Invoice_Number>
<Invoice_Date>31-07-2020</Invoice_Date>
<Terms_Payment>Net 15</Terms_Payment>
<Company_Name>ABC company</Company_Name>
<Billing_Contact>ABC-Contact1</Billing_Contact>
<Address>New york, United State</Address>
<Email>support@edocgen.com</Email>
<Logo>621cd2b783a6095d7b15a443</Logo>
<Sum1>6,751</Sum1>
<para>61b334ee7c00363e11da3439</para>
<ITH>
<Heading1>Item Description</Heading1>
<Heading2>Amount</Heading2>
</ITH>
<IT>
<Item_Description>Product Fees: X</Item_Description>
<Amount>5,000</Amount>
</IT>
</values>
<marker>
我为 XML 作为数据源所做的代码更改很简单,如下所示
JavaScript
var formBody = new FormData();
formBody.append("documentId", documentId);
formBody.append("format", outputFormat);
formBody.append("outputFileName", fileName);
formBody.append("inputFile", fs.createReadStream("./XML_Invoice.xml"));
从数据库生成文档几乎与其他数据源相同。但在这种情况下,我们需要提供连接详细信息和 SQL 查询,而不是上传 inputFile。
SQL 查询的输出列应与文档模板中的标签匹配。
让我们看看如何在代码中进行配置。
JavaScript
const templateId = "<template id>";
const dbVendor = "mysql";
const dbUrl = "<jdbc connection URL>";
const dbLimit = "100";
const dbPassword = "<database password>";
const dbQuery = "SELECT JSON_ARRAY(first, last) FROM customers;";
const outputFormat = "pdf";
// form data prepareation
let formBody = new FormData();
formBody.append("documentId", templateId);
formBody.append("format", outputFormat);
formBody.append("dbVendor", dbVendor);
formBody.append("dbUrl", dbUrl);
formBody.append("dbLimit", dbLimit);
formBody.append("dbPassword", dbPassword);
formBody.append("dbQuery", dbQuery);
formBody.append("outputFileName", fileName);
其他一切都将保持不变。
eDocGen 提供了通过电子邮件发送生成的文档的功能。
应用程序接口:POST-/api/v1/output/email
出局 | 将需要通过电子邮件发送的输出 ID 放在这里 |
电子邮件ID | 将用户电子邮件放在这里 |
内容类型 | 多部分/表单数据 |
x-访问令牌 | 来自登录的 JWT 身份验证令牌 |
let login = require("../edocgen_login");
let axios = require("axios");
const hostName = "https://app.edocgen.com/api/v1/output/email";
const headers = {
"Content-Type": "application/json",
"x-access-token": "null",
};
const outId = "<output ID>"; // Put output ID here which need to be sent via email
const emailId = "<user email>"; // Put user email here
module.exports.generateFiles = function () {
let authToken = login.getToken(function handleUsersList(token) {
headers["x-access-token"] = token;
let payload = { outId: outId, emailId: emailId };
let config = {
method: "post",
url: hostName,
headers: headers,
data: payload,
};
axios(config)
.then(function (response) {
console.log("Mail sent");
})
.catch(function (error) {
console.log(error);
});
});
};
来自 eDocGen 的电子邮件如下所示
还有很多其他的功能我在这里无法涵盖。但我希望这篇文章可以为您提供一个从哪里开始的想法。
前一阵儿被某网站的 JS 反爬流程难住了,至今也没明白它的反扒原理和攻破方法。最终找到了一个自动化脚本工具 autoit 3,用一个笨方法将人手动点击浏览器的动作脚本化,达到网页数据获取目的,拿到网页文件后,再用代码解析,曲线完成任务。
本文将介绍这个自动化的过程,并带编写一个完整的 autoit 3 爬虫脚本,希望对各位读者朋友有所启发。
以国家信息安全漏洞共享平台为例,它在返回数据前发起了两次 512 响应,第三次浏览器带着动态生成的 Cookie 信息才能得到数据。
这次咱们直接从网页入手,操作键盘找到“下一页” 按钮,按下 Enter 键完全请求。通过键盘定位到 “下页” 按钮的过程为:
接着就可以编写自动化脚本了,把刚刚的手动操作翻译成脚本命令:
这个流程,对其他高反扒的信息发布网站,也是适用的。
按照上面的流程,编写 autoit 自动化脚本,创建一个 myspider.au3 文件:
#include <AutoItConstants.au3>
;;切换为英文输入法,保证浏览器输入正常
$hWnd = WinGetHandle("[ACTIVE]");$hWnd 为目标窗口句柄,这里设置的是当前活动窗口
$ret = DllCall("user32.dll", "long", "LoadKeyboardLayout", "str", "08040804", "int", 1 + 0)
DllCall("user32.dll", "ptr", "SendMessage", "hwnd", $hWnd, "int", 0x50, "int", 1, "int", $ret[0])
$url = "https://www.cnvd.org.cn/flaw/list.htm"
spiderData($url)
Func spiderData($url)
;;打开 Chrome 浏览器窗口
$chromePath = "C:\Users\admin\AppData\Local\Google\Chrome\Application\chrome.exe"
Run($chromePath)
;;登录窗口显示
WinWaitActive("[CLASS:Chrome_WidgetWin_1]")
;; 休息2000毫秒
Sleep(2000)
;; 移动窗口
WinMove("[CLASS:Chrome_WidgetWin_1]", "打开新的标签页 - Google Chrome", 0, 0,1200,740,2)
;; 休息500毫秒
Sleep(500)
;;地址栏输入URL 并按下 Enter 键
Send($url)
Sleep(500)
Send("{enter}")
Sleep(3000)
;; 循环爬取需要的页数,测试只爬3页
For $i = 1 To 3 Step 1
;;打开右键另存为按钮: Ctrl+S
send("^s")
Sleep(2000)
WinWait("[CLASS:#32770]","",10)
;;将存储路径设置到另存为组件输入框 Edit1 里
$timeNow = @YEAR & "" & @MON & "" & @MDAY & "" & @HOUR & "" & @MIN
$savePath = "F:\A2021Study\ListData\" &$timeNow & "_page" & $i & ".html"
ControlSetText("另存为","", "Edit1", $savePath)
;;点击确定
ControlClick("另存为","","Button2")
;;再次确定
WinWait("[CLASS:#32770]","",10)
ControlClick("确认另存为","","Button1")
;; 等待保存操作完成
Sleep(3000)
;; 定位到下一页按钮,并触发点击下一页
send("{END}")
Send("+{TAB 15}")
Send("{enter}")
;;点击确定后,等待网页加载完成
Sleep(3000)
Next
;; 整个操作完成,则关闭浏览器
Send("^w")
EndFunc
脚本编写过程中,有几点需要注意:
因为爬虫要作为定时任务运行的,为避免打开太多浏览器窗口,因此需要在脚本结束时关闭浏览器。
数据爬取一般分为列表页和详情页,定位点击每一条详情的过程比较麻烦,所以爬取详情页面的和列表分开,用 Java 代码解析所有详情 URL 后,再由另一个 autoit 脚本去获取详情页面,这个流程大家可以自己写一下,这里就不详细介绍了。
最后再汇总下整个爬取的流程:
第一步,执行爬取列表的 autoit 脚本,得到列表页面 html;
第二步,解析列表页 html ,得到所有详情页面的 URL ,写入到文件中;
第三步,执行爬取详情页面的 autoit 脚本,它遍历第二步的目标 URL ,得到详情页 html ;
第四步,解析详情页 html 文件,得到详情数据。
总控流程、第二步和第四步的解析都用 Java 代码完成,用 Runtime.getRuntime().exec("cmd /c E:\A2021Study\Autoit3\myspider.au3") 调用脚本,文件路径是反斜杠。
这个方法虽然有点笨,但完全是人工操作浏览器,能够对抗反爬虫策略,感兴趣的朋友可以执行下本文的脚本试试。
autoit 还是蛮有意思的,语法也很简单,DirCreate 创建文件,iniread 读取配置项,一行代码顶 Java 几十行,不得不承认 Java 操作文件才是最麻烦的哇!
:如何用 JS 一次获取 HTML 表单的所有字段 ?
考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:
<form>
<label for="name">用户名</label>
<input type="text" id="name" name="name" required>
<label for="description">简介</label>
<input type="text" id="description" name="description" required>
<label for="task">任务</label>
<textarea id="task" name="task" required></textarea>
<button type="submit">提交</button>
</form>
上面每个字段都有对应的的type,ID和 name属性,以及相关联的label。用户单击“提交”按钮后,我们如何从此表单中获取所有数据?
有两种方法:一种是用黑科技,另一种是更清洁,也是最常用的方法。为了演示这种方法,我们先创建form.js,并引入文件中。
首先,我们在表单上为Submit事件注册一个事件侦听器,以停止默认行为(它们将数据发送到后端)。
然后,使用this.elements或event.target.elements访问表单字段:
相反,如果需要响应某些用户交互而动态添加更多字段,那么我们需要使用FormData。
首先,我们在表单上为submit事件注册一个事件侦听器,以停止默认行为。接着,我们从表单构建一个FormData对象:
const form = document.forms[0];
form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
});
除了append()、delete()、get()、set()之外,FormData 还实现了Symbol.iterator。这意味着它可以用for...of 遍历:
const form = document.forms[0];
form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
for (const formElement of formData) {
console.log(formElement);
}
})
除了上述方法之外,entries()方法获取表单对象形式:
const form = document.forms[0];
form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
const entries = formData.entries();
const data = Object.fromEntries(entries);
});
这也适合Object.fromEntries() (ECMAScript 2019)
为什么这有用?如下所示:
const form = document.forms[0];
form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(this);
const entries = formData.entries();
const data = Object.fromEntries(entries);
// send out to a REST API
fetch("https://some.endpoint.dev", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
}
})
.then(/**/)
.catch(/**/);
});
一旦有了对象,就可以使用fetch发送有效负载。
小心:如果在表单字段上省略name属性,那么在FormData对象中刚没有生成。
要从HTML表单中获取所有字段,可以使用:
使用FormData构建具有所有字段的对象,之后可以转换,更新或将其发送到远程API。*
作者:VALENTINO GAGLIARDI 译者:前端小智 来源:valentinog
原文:https://www.valentinog.com/blog/form-data/
*请认真填写需求信息,我们会在24小时内与您取得联系。