用 Cricket 在 Java 环境里构建极简的内容管理服务器

原文作者:Grzegorz Skorupa

原文地址:https://dzone.com/articles/minimalistic-cms-microservice-for-java

Cricket 是一个用来快速创建 Java 微服务的平台。它在最新的 “Microsite” 发布版里面提供了一套现成的功能,让我们可以很轻松地部署 CMS (Content Management Server,内容管理服务器)或其他 Web 应用。我们也可以在更大的应用的前端或后端里用到这一平台。

Cricket 有着模块化、事件驱动式的架构,可用于构建在 Raspberry PI Zero 等平台上运行的微型服务,也能利用云系统的底层设备部署一个分布式的系统。

本文便会展示使用该平台构建一个简单的 CMS 来管理网站的方式。我们还会展示 Docker Hub 自带的 Docker 容器的使用方法,还会在末尾部分介绍直接在 Java 环境里面运行这一平台的方法。

目录

  1. 怎么还要一个 CMS?
  2. 如何在 5 分钟之内弄好一个网站
  3. Cricket 平台的架构
  4. 用 Cricket 来构建 CMS
  5. 如何用 CMS 来管理网站
  6. 如何根据需求来运行 CMS
  7. 补充说明

1. 怎么还要一个 CMS?

Java 开发者面临着一个艰难的选择。在运行网站或者 Web 应用的时候,如果要一个快速的、轻量级的解决方案的话,那么面向 Java 的 Web 及门户(portal)的开发平台就会因显得规模过大、设计过度,且消耗系统资源过高而备受诟病。也因为这点,我们会经常去跟集合了很多门技术的系统打交道,其中 Java 比较可能用在后端。

这一多种技术的组合既可以很完美地实现目标,也可以为开发者带来新的挑战,从而丰富自身的知识。然而,随着技术的种类以及系统复杂度的增加,扩增技术栈、更改功能或是向团队引入新人所带来的风险还有问题也会越来越多。

微服务概念的出现让我们很自然地产生了对快速构建原型和减少系统资源需求的期望,而 Cricket 微网站平台的出现便在某种意义上响应了这种期望。它为架构师还有 Java 开发者提供了一套可以一用的工具。

2. 如何在 5 分钟之内弄好一个网站

Cricket 平台最简单的用法就是跑一个静态网页应用。我们用现成的 Docker 镜像就可以在几分钟内弄好一个网站,而不用额外安装任何软件。

比如说,我们可以新建一个 mywww 文件夹,然后在里面放一个简单的 HTML 页面,然后运行 Docker Hub 里面的 "cricket-micosite" 镜像:

$ mkdir mywww
$ echo "Hello!">mywww/index.html
$ docker run --name mycricket -d -v "$(pwd)"/mywww:/cricket/www \
-p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest

Cricket 内置了一个 HTTP 服务器,后者会默认监听 8080 端口并会对外提供 /cricket/www 及其子目录里面的所有文件。在上面的例子里面,我们的网站便能通过 URL http://127.0.0.1:8080 来访问。现在我们就能试着去修改 index.html 文件,或是添加别的页面和文件了。

注意:以这种方式启动的平台不会自动刷新内部缓存,因此只有在重启容器之后,文件中的所有更改才会在浏览器中可见。

我们可以通过以下命令来停止一个正在运行的 Docker 容器:

$ docker stop mycricket

若要启动处于停止状态的容器,就使用这个命令:

$ docker start mycricket

3. Cricket 平台的架构

在我们讨论下一个例子之前,我们先来看看这一平台的关键组件。

Cricket 实现了一种 “端口与适配器”(见下图六边形)的架构。这种架构下的系统能调用特定的适配器来实现一些服务的功能。除此之外,这一方案还有个优势,那便是它能轻松安全地更改单个适配器的实现方法。比如我们可以很方便地更改数据库的类型,或者用一个外部消息中继器来取代内置的解决方案。

微网站会用这些服务所对应的适配器:

  • AuthService - 负责 CM 模块中的用户授权
  • UserService - 用户管理
  • ContentManager(CM)- 内容管理
  • ContentService(CS)- 提供来自 CM 模块的已发布内容
  • WwwService - 处理和提供内容来响应 HTTP 请求

UserService,AuthService,ContentService 和 ContentManager 对应的适配器使得这些服务可以通过 REST API 来调用。

注意:在默认情况下,微网站是作为一个服务提供的,但微网站也可以分成几个更小的微服务,从而提供更好的可扩展性并提高实施解决方案的安全性。

4. 用 Cricket 来构建 CMS

内容管理系统(CMS,或者叫 WCM,即 Web 内容管理)简化了对网站内容的管理,能帮助我们在不用理会它的布局的前提下修改显示在网站里面的内容。

Cricket Microsite 也属于这一类系统,但和其中一些系统不同。它的功能有限,使其难以被商业级别的用户所接受(例如其缺乏版本控制和对工作流程的简化,只能编辑或删除已经发布的内容),而它主要针对的是程序员和一般的网站设计者。

CM 模块

CM(ContentManager)模块负责管理存储在数据库中的内容元素(即文档),让我们能创建、编辑、发布、取消发布以及删除这些元素。具有管理员或者编辑者角色权限的登录用户可以访问这一模块。

我们会区分三种类型的文档:

  • FILE - 各种文件(如照片)
  • CODE - 用户可以在 CM 的界面中编辑的代码(例如 CSS,JavaScript,HTML)
  • ARTICLE - 可嵌入到网页上的可编辑内容

文档的主要特点有:

  • 每个文档会由一组参数组成,而 FILE 类型的文档会链接到另外一个文件。
  • 文档在系统中有一对参数 uid + language 来作为其唯一标识。其中 uid 又由文档的路径和文档名两个参数组合得到。
  • 一个文档可以处于以下两种状态之一:
    • wip(即草稿)- 仅对登录 CM 模块的用户可见
    • published - 已通过 ContentService 或 WwwService 公开发布(会显示在网站上)
  • 具有相同 uidlanguage 参数不同的文件其实是同一个文档,只是它们的显示语言不同。

启动平台

在前面的示例里面,我们用这个平台(使用了 -v 参数来启动的容器)来发布了指定的文件夹里面的页面,可是在这一模式下,ContentManager 模块是不可用的。因此,为了体验完整的功能,我们应该在不挂载本地文件夹的前提下启动 Docker 容器:

$ docker run --name mycricket -d -p 127.0.0.1:8080:8080 \
gskorupa/cricket-microsite:latest

容器会自动地在本地的文件系统中创建两个分卷来存储工作所需的文件(比如数据库还有页面的模板)。我们也可以通过这个命令找出这两个分卷的实际位置:

$ docker container inspect mycricket

本文在末尾部分给出了一些对平台启动方式的补充说明。

创建和编辑文档

在系统完成启动之后,我们就能用初始账号 admin 和初始密码 cricket 登录到 ContentManagement 模块(https://127.0.0.1:8080/cricket)里面。

在菜单选中选项 Content > Documents 之后,我们就能在屏幕上看到符合特定状态和语言条件的文件的列表:

Content Manager 管理模块是基于 Riot 和 Bootstrap 库所构建的,并且支持了 RWD 应用开发技术,因此我们也能用智能手机打开这个界面。

发布

在我们保存文档后,文档状态还只是 wip,只有能访问 ContentManager 模块的用户才能访问这个文档。我们要通过点击 “Status” 图标,把文档发布出去才能让文档被所有访问者看到。把文档的状态改成已发布是不需要确认的。只要点击这个图标,文档就会从 wip 文档列表里消失。

文档的取消发布操作也是以类似的方式完成的。

注意:发布的文档可以编辑,并且对文档的编辑在保存之后立即生效,能被所有访问者见到!

5. 如何用 CMS 来管理网站

Cricket Microsite 可以将静态网页应用平滑地迁移到一套 WCM 的解决方案里面。这要归因于其内置的 HTTP 服务器特有的文件提供方式。

在接收到对指定文件的特定路径的 GET 请求时,服务器会首先搜索标识符(UID)里的路径参数与给定路径相同的 FILE 或 CODE 类型的文档。对这一类文档的搜索位置的优先顺序如下:

  • WwwService 适配器缓存
  • ContentService 适配器数据库
  • 内部文件系统

正是上述方式让我们能用一个选定模板里面的文件快速地运行一个静态网站的原型,然后用 CM 模块里面的相应文档来连续地替换磁盘上的文件。

举例

Cricket 有个内置的默认页面模板(可以在 http://127.0.0.1:8080 里面看到)。这里我们就用自己的动态网站来替换它。这一过程有以下步骤:

1. 登录到 CM 模块并用以下参数创建一个新文档:

参数

type

CODE

name

index.html

path

/

content

Hello!

MIME type

text/html

2. 保存文档并发布

3. 检查主页(http://127.0.0.1:8080)的内容是否已更改为上述文档所指定的内容

注:在以这一章节的模式启动时无需重启服务。CM 模块中的文档更改会立即显示在网站上。

4. 在 CM 模块中找到已发布文档的列表,然后编辑其中的 index.html 文档。在 “Content” 字段中输入如下所示的 HTML 代码:

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="theme.css">
    </head>
    <body>
        <img src="/images/logo.png"/>
        <h1>Hello World!</h1>
    </body>
</html>

5. 保存并检查页面有没有改变

6. 创建一个样式定义表文档:

参数

type

CODE

name

theme.css

path

/

MIME type

text/css

body {
    color: darkblue;
    text-align: center;
}

7. 创建一个作为 logo 的文档:

参数

type

CODE

name

logo.png

path

/images

MIME type

image/png

file

从硬盘里选出来的文件

8. 发布所有创建的文档后,我们的网站应该会变成这个样子:

我们还可以用同样的方式来创建或管理后续的页面,从而根据需求扩展我们的网站。

6. 如何根据需求来运行 CMS

通过改代码的方式来在页面上发布信息是很不方便的。像 WordPress 这样的 WCM 平台会采用一种避免修改网页源代码的内容编辑方式来简化这个过程。这也使网站开发和文档编辑的分离成为了可能。

在 Cricket Microsite 里面,我们也能利用 ARTICLE 类型的文档来达成相同的效果。

得益于这一平台的特性,我们就可以根据自己的需求设计并构建解决方案,而不会有过分的系统资源需求,并且能比一体化的系统更加易用。

描述关于设计动态网页、网页间的导航、搜索文档还有响应用户操作的所有方面的需求已经远远超出了本文的范围。这里我们只描述一个解决方案的例子。有经验的读者应该能在此基础上开发出自己的一套方案。

必备功能

我们将部署一个有这些功能的 RWD 新闻网站:

  • 菜单
  • 主页,用于展示介绍和最新消息(文章)的列表
  • 新闻页面
  • 展示文章全文内容的页面

使用技术

为了开发页面模板,我们会用到以下类库。它们非常符合 Cricket Microsite 的极简理念:

  • Bootstrap 4.0 - 能根据 RWD 轻松地构建页面的 HTML 代码。
  • Riot - 帮助我们将数据呈现到用户界面的视觉元素上。

构建模块

我们的网站将由存储在内容管理模块中的几种文档构建而成:

  • 页面模板
  • Riot 组件
  • 样式表(CSS)
  • 新闻文章

UID

目的

/index.html

主页模板

/local/js/routing.js

Riot 路由组件

/local/components/app_main.tag

Riot 组件,作为主页

/local/components/app_menu.tag

Riot 组件,显示菜单

/local/components/app_list.tag

Riot 组件,显示文章列表

/local/components/app_articleview.tag

Riot 组件,下载并显示选定的消息

/local/components/app_article.tag

Riot 组件,呈现 ARTICLE 类型文档的内容

/tags/raw.tag

Riot 组件,让我们能将 HTML 片段注入另一个组件里面。

/local/css/styles.css

样式表

/welcome_text

显示在主页上的内容

/news/art1

新闻示例 1

/news/art2

新闻示例 2

注:raw组件(raw.tag)和数据访问功能(data-api.js 文件)是 Cricket Microsite 发行版的固有部分,在此之后不作讨论。

主页模板

这以文件的任务是加载所需的 JavaScript 库,样式表和 Riot 组件。在它被创建之后,它只有在修改组件列表时才需要修改。

参数

type

CODE

name

index.html

path

/

MIME type

text/html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <!-- Custom styles -->
        <link rel="stylesheet" href="/local/css/styles.css">
    </head>
    <body>
      <app_main></app_main>
      <!-- SCRIPTS: jQuery first, then Bootstrap JS, then Riot -->
      <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/riot/3.9.3/riot+compiler.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/riot-route@3.1.2/dist/route.min.js"></script>
      <!-- Application scripts -->
      <script src="/local/js/routing.js"></script>
      <script src="/js/data-api.js"></script>
      <!-- Riot components -->
      <script data-src="/tags/raw.tag" type="riot/tag"></script>
      <script data-src="/local/components/app_article.tag" type="riot/tag"></script>
      <script data-src="/local/components/app_main.tag" type="riot/tag"></script>
      <script data-src="/local/components/app_menu.tag" type="riot/tag"></script>
      <script data-src="/local/components/app_list.tag" type="riot/tag"></script>
      <script data-src="/local/components/app_articleview.tag" type="riot/tag"></script>
      <script>
        app.csAPI = "http://localhost:8080/api/cs";
        riot.mount('*');
        route.start(true);
      </script>
    </body>
</html>

Riot 组件

路由功能 是由与页面间转移有关的事件(单击菜单里的链接,刷新页面,或者按退格键等)所触发的。它让我们能定义与导航相关的在浏览器中的活动。

参数

type

CODE

name

routing.js

path

/local/js

MIME type

text/javascript

route(function (id) {
  if(id.startsWith('news')){
    app.currentPage='news'
    app.selectedDoc = id.substring(4).replace(/,/g , '/')
  }else{
    app.currentPage='home'
    app.selectedDoc=''
  }
  globalEvents.trigger('pageselected')
  riot.update()
})

app_main 组件负责把组件嵌入到页面中,并基于应用的当前状态调整组件的可见性:在这里的例子则是根据选定的页面和文档来调整。其文档参数为:

参数

type

CODE

name

app_main.tag

path

/local/components

MIME type

text/html

<app_main>
  <app_menu></app_menu>
  <app_articleview if={app.currentPage=='home'} uri='/welcome_text' mode='intro'></app_articleview>
  <app_list if={app.currentPage=='home'} limit='1'></app_list>
  <app_articleview if={app.currentPage=='news' && app.selectedDoc!=''} uri={app.selectedDoc} mode='view'></app_articleview>
  <app_list if={app.currentPage=='news' && app.selectedDoc==''}></app_list>
</app_main>

app_menu 组件则会展示一个简单的菜单。

参数

type

CODE

name

app_menu.html

path

/local/component

MIME type

text/html

<app_menu>
  <div class="container menu">
    <div class="row" >
      <div class="col">
        <a href="/">Home</a> <a href="#news">News</a>
      </div>
    </div>
  </div>
</app_menu>

app_list 会使用 CS 模块的 REST API 加载特定路径下的文档列表,然后使用 app_article 组件呈现这些文档的链接。

参数

type

CODE

name

app_list.html

path

/local/component

MIME type

text/html

<app_list>
  <div class="container">
    <virtual each={item in list}>
      <div class="row">
        <div class="col">
          <app_article title={item.title} summary={item.summary} mode='list' page='#news' uid={ item.uid } />
        </div>
      </div>
    </virtual>
  </div>
  <script>
    var self=this
    self.limit=0
    self.list=[]
    self.on('mount', function(){
      self.limit=opts.limit
      loadDocs()
    })
    var loadDocs = function () {
      getData(app.csAPI + '?path=/news/&language=' + app.language, null, null, setDocList, self)
    }
    var setDocList = function (text) {
      self.list = JSON.parse(text)
      if(self.limit>0){
        self.list=self.list.slice(0,self.limit)
      }
      var i
      for (i = 0; i < self.list.length; i++) { 
        self.list[i]=decodeDocument(self.list[i])
      }
      self.update()
    }
  </script>
</app_list>

app_articleview 则使用 CS 模块的 REST API 加载指定 uid 的文章,然后将代表此文档的 JSON 对象呈递给 app_article 组件。

参数

type

CODE

name

app_articleview.tag

path

/local/component

MIME type

text/html

<app_articleview>
  <div class="container">
    <div class="row" >
      <div class="col">
        <app_article title={doc.title} mode={mode} />
      </div>
    </div>
  </div>
  <script>
    var self=this
    self.doc={ title: '.....'}
    self.on('mount', function(){
      self.uri=opts.uri
      self.mode=opts.mode
      self.articleTag=self.tags.app_article
      getData(app.csAPI + self.uri+'?language=en', null, null, setDocument, self)
    })
    var setDocument = function (text) {
      self.doc = decodeDocument(JSON.parse(text))
      self.articleTag.update(self.doc)
      self.articleTag.update({mode:self.mode})
    }
  </script>
</app_articleview>

app_article 组件的任务是使用适当的格式来将选定的文档嵌入到网页里面。我们所选的格式会根据文档的参数还有链接来决定呈现文档的具体样式。

参数

类型

CODE

名称

app_article.tag

路径

/local/component

MIME 类型

text/html

<app_article>
  <article class='standard'>
    <header>
      <h1 class={mode}>{title}</h1>
      <div class='intro' if={summary}><raw html={summary}/></div>
    </header>
    <div if={content}><raw html={content}/></div>
    <footer>
{ published }</div></footer>
    <div if={ mode=='view' }><a href="#" onClick="history.back()" }>Back</a></div>
    <div  if={ mode=='list' }><a href={detailsLink}>More ...</a></div>
  </article>
  <script>
    var self=this
    self.title=opts.title
    self.summary=opts.summary
    self.content=opts.content
    self.author=opts.author
    self.published=opts.published
    self.type=opts.type
    self.page=opts.page
    self.uid=opts.uid
    if(opts.mode){
      self.mode=opts.mode
    }else{
      self.mode='default'
    }
    self.detailsLink=''+self.page+self.uid
    self.detailsLink=self.detailsLink.replace(/\//g , ',')
  </script>
</app_article>

样式表

样式表文档可用于管理网站的外观。

参数

type

CODE

name

styles.css

path

/local/css

MIME type

text/css

article > header {
  margin-top: 20px;
}
article > header > h1 {
  font-size: x-large;
  font-weight: bold;
}
article > header > h1.view {
  font-size: xx-large;
  font-weight: bold;
  color: green;
}
.menu {
  padding: 10px;
  background: linear-gradient(white,lightgray);
}

内容

我们在这里准备的主页会显示存储在 CM 模块中的标识符为 /welcome_text 的文档的内容(即在 / 路径下,名为 welcome_text 的文档)。若现在还没有弄好这个文档的话,现在也是时候准备了。

首页还会显示 /news 路径中的所有文章的列表。为了简单起见,这里的代码没有限制文档的显示数量,也没有分页机制。

结果

若上述的所有步骤均已完成,那么我们就可以在 http://127.0.0.1:8080 上看到我们的成果:

7. 补充说明

完整文档参数列表

参数

含义

uid

唯一的文件标识符(路径 + 名称)

path

文档在存储库结构中的位置(路径)

name

特定路径下的文档的唯一名称(只包含字母数字,不含空格)

type

文档类型(ARTICLE / CODE / FILE)

author

文档作者

title

文档的标题

summary

文档的摘要

content

文档的内容。对 FILE 类型的文档对应文件在平台的本地文件系统的路径

tag

(暂无用处)

language

用两个字符表示的文档语言代码

commentable

(暂无用处)

MIME type

文档的 MIME 类型

status

文档状态(wip / published)

size

文档大小

created

创建日期

modified

最后一次修改的日期

published

上次发布的日期

createdBy

登录文档创建者

Docker 容器运行选项

  1. 标准运行选项
$ docker run --name cricketsite -d -e CRICKET_URL='https//www.mysite.com' \
 -p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest

运行参数

含义

-- name

会给容器指定一个便于我们进行识别的名称。

-d

在后台运行容器

-e

设置容器的环境变量值。例如,环境变量 CRICKET_URL 会存储我们服务的 URL 地址。如果要从浏览器异步引用服务(CM 模块的 Web 应用所使用的机制),则必须设置此变量。

-p

端口映射。我们可以在此指定 Cricket 的 HTTP 服务器所监听的 IP 地址以及由 Docker 来暴露的端口号。

  1. 本地文件服务器模式(带有连接分卷的选项)
$ docker run --name mycricket -d -v "$(pwd)"/mywww:/cricket/www \
-p 127.0.0.1:8080:8080 gskorupa/cricket-microsite:latest

参数

含义

-v

将本地文件系统中的指定文件夹连接到容器里面来作为容器的一个分卷

在 JVM 上运行 Cricket 平台

如果不想用 Docker,也可以直接在 JRE 上运行这一系统。这对希望用这个平台的人来进行一些测试(比如说:修改静态页面的模板,更改数据库,或是使用自己的后端组件来扩展这一平台等)的人来说也是一种可行的选择:

1. 下载最新版本的平台并将其解压到我们选择的文件夹。

$ wget https://github.com/gskorupa/Cricket/releases/download/1.2.41/cricket-microsite.zip
$ mkdir myservice
$ unzip cricket-microsite.zip -d myservice

2. 开始服务

$ cd myservice
$ sh run.sh

注:服务的运行参数只适用于 Java 9 / 10,但我们可以通过编辑 run.sh 文件来更改它。

3. 根据需要修改和扩展服务。

我们可以直接修改 myservice/work/www 这一路径中的文件结构。

外部资源

https://www.cricketmsf.org/

https://docker.io/

https://hub.docker.com/

https://getbootstrap.com/

http://riotjs.com/

https://en.wikipedia.org/wiki/Responsive_web_design

本文的版权归 Tnecesoc 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏破晓之歌

15款Django开发常用软件包 原

1. Python social auth 一款社交账号认证/注册机制,支持Django、Flask、Webpy等在内的多个开发框架,提供了约50多个服...

382
来自专栏向治洪

Flutter环境搭建

这几年,移动跨平台的趋势可以说是越来越明显,技术实现上也是百花争艳,不过究其实现,无外乎有那么几种。 Web 流:也被称为 Hybrid 技术,它基于 Web ...

4177
来自专栏GopherCoder

『Go 语言学习专栏』-- 第十四期

1733
来自专栏葡萄城控件技术团队

Webpack4干货分享(一):入口、输入和ES6模块

你好!今天我们会开始一个 Webpack 4的入门教程。我们会以Webpack的基本概念开始,随着教程逐渐深入。这一次,我们将学习用ES6 modules进行模...

900
来自专栏FreeBuf

利用雅虎小型企业服务平台的目录遍历漏洞查看客户的信用卡信息

在这篇文章中,我将跟大家介绍如何利用雅虎小型企业服务平台的目录遍历漏洞查看客户的信用卡信息。在过去的一年半时间里,我一直都在对雅虎平台的安全性进行分析,而本文所...

2317
来自专栏微信小程序开发

微信小程序开发常见问题(五)

知晓程序员,专注微信小程序开发的程序员! 一、微信小程序审核未通过,怎么办? 小程序审核不通过的原因很多,微信会给出相应审核不通过 的原因。今天连胜老师给大家...

3147
来自专栏Crossin的编程教室

Python-Excel 模块哪家强?

0. 前言 从网页爬下来的大量数据需要清洗? 成堆的科学实验数据需要导入 Excel 进行分析? 有成堆的表格等待统计? 作为人生苦短的 Python 程序员,...

3235
来自专栏AndroidTv

【Android】又一个Gank客户端来啦

Gank平台的移动端又来了,非常感谢Gank平台开放接口,让我们这些小白有机会练手、学习。

3516
来自专栏理论坞

UI(用户界面)设计规则和规范

界面是软件与用户交互的最直接的层,界面的好坏决定用户对软件的第一印象。而且设计良好的界面能够引导用户自己完成相应的操作,起到向导的作用。同时界面如同人的面孔,具...

793
来自专栏啸天"s blog

天若ocr文字识别工具,集合百度、腾讯、有道、搜狗

1、对于搜狗的接口调用的还是http://ocr.shouji.sogou.com/v2/ocr/json,这个接口识别效果很好,但是对于图片的尺寸有规定。 ...

1454

扫码关注云+社区