存储 URL 到文件
序言
在前两篇文章中,URL 以及短链的 ID 都是存储在内存中的,程序重启后,已经绑定好的 URL 和 ID 就都丢失了。本章介绍如何将短链的信息存储到文件中,并在程序启动时加载进内存中。
写入文件
在之前的文章中,url_to_id
是一个动态谓词,可以将其简单地理解为一个既可以根据Url
查询出Id
、也可以根据Id
查询出Url
的双向哈希表。为了可以实现将短链信息存储到文件中,将会把url_to_id
修改为一个预先定义好的谓词。
url_to_id
仍然接受Url
和Id
两个参数,但这两个参数在实际使用时,不会同时都有值、总有一个是变量:
- 如果
Url
是字符串、Id
是整数,那么实现的是将新的 URL 存储起来的功能; - 如果
Url
是变量、Id
是整数,那么它会实例化Url
,这样实现的是根据 ID 查找原始 URL 的功能。
用于存储短链信息的文件格式为 CSV,第一列为 ID,第二列为原始 URL,而两者之间通过逗号分隔。这样的一个短链服务的代码可能是下面这样子的
:- initialization(main, main).
:- use_module(library(http/http_dispatch), [http_404/2, http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).
:- use_module(library(lists), [nth0/3]).
:- use_module(library(readutil), [read_file_to_string/3]).
find_url_by_id(_, [], _) :- fail.
find_url_by_id(Id, [Line|Lines], Url) :-
split_string(Line, ",", "", SubStrings), % 将一行内容按照逗号切割,放到 SubStrings 中。
nth0(0, SubStrings, IdString), % 取出第一列的数据,放到 IdString 中。
( format(string(IdString), "~d", [Id]) % 检查将 Id 转换为字符串的话,是否与 IdString 相同。
-> nth0(1, SubStrings, Url) % 如果相同,说明第二列的数据就是所需要的链接,放入变量 Url。
; find_url_by_id(Id, Lines, Url)). % 否则,递归处理 Lines,直至为空、查询失败。
% 读写文件来管理短链。
url_to_id(Url, Id) :-
string(Url),
integer(Id),
open("/tmp/url.csv", append, Stream),
format(string(Line), '~d,~s~n', [Id, Url]),
write(Stream, Line),
close(Stream).
url_to_id(Url, Id) :-
read_file_to_string("/tmp/url.csv", String, []),
split_string(String, "\n", "", Lines),
find_url_by_id(Id, Lines, Url).
% 辅助函数。
reply_by_id(Id, Request) :-
( url_to_id(Url, Id)
-> http_redirect(moved, Url, Request)
; http_404([], Request)).
lengthen_url(Request) :-
http_parameters(Request, [
id(Id, [integer])
]),
reply_by_id(Id, Request).
shorten_url(Request) :-
http_parameters(Request, [
url(Url, [string]) % 因为 url_to_id 中有类型要求。
]),
% 姑且用当前时间戳来作为 ID。
get_time(TimeStamp),
Id is truncate(TimeStamp),
url_to_id(Url, Id),
format('Content-Type: application/json~n~n'),
format('{"id": ~d}', [Id]).
main(_) :-
http_handler('/api/lengthen', lengthen_url, [methods([get])]),
http_handler('/api/shorten', shorten_url, [methods([post])]),
http_server([port(8082)]),
sleep(100000000).
确保文件流关闭
在url_to_id
的定义中,并没有机制可以确保close
一定会被调用来关闭文件流。要实现类似 Python 中的with open
、或者 Common Lisp 中的with-open-file
的效果的话,可以用setup_call_cleanup
。因此,url_to_id
的第一个分支可以修改为
url_to_id(Url, Id) :-
string(Url),
integer(Id),
setup_call_cleanup(
open("/tmp/url.csv", append, Stream),
(
format(string(Line), '~d,~s~n', [Id, Url]),
write(Stream, Line)
),
close(Stream)
).
分隔源文件
到目前为止,这个 HTTP 服务的源文件已经有 60 行左右了,而且其中的谓词url_to_id
也是一个相对独立的功能——它与处理 HTTP 请求并没有太大关系,因此很适合将其剥离到另一个源文件中。新的文件结构将会是这样
/
|- http_server.pl
`- url_to_id.pl
其中,在文件url_to_id.pl
中,使用指令:- module
定义出一个模块,并将其中的谓词url_to_id/2
暴露出来、供http_server.pl
使用,后者则使用指令:- use_module
来加载这个自定义模块。最终url_to_id.pl
和http_server.pl
的内容分别如下
:- module(url_to_id, [url_to_id/2]).
:- use_module(library(lists), [nth0/3]).
:- use_module(library(readutil), [read_file_to_string/3]).
find_url_by_id(_, [], _) :- fail.
find_url_by_id(Id, [Line|Lines], Url) :-
split_string(Line, ",", "", SubStrings), % 将一行内容按照逗号切割,放到 SubStrings 中。
nth0(0, SubStrings, IdString), % 取出第一列的数据,放到 IdString 中。
( format(string(IdString), "~d", [Id]) % 检查将 Id 转换为字符串的话,是否与 IdString 相同。
-> nth0(1, SubStrings, Url) % 如果相同,说明第二列的数据就是所需要的链接,放入变量 Url。
; find_url_by_id(Id, Lines, Url)). % 否则,递归处理 Lines,直至为空、查询失败。
% 读写文件来管理短链。
url_to_id(Url, Id) :-
string(Url),
integer(Id),
setup_call_cleanup(
open("/tmp/url.csv", append, Stream),
(
format(string(Line), '~d,~s~n', [Id, Url]),
write(Stream, Line)
),
close(Stream)
).
url_to_id(Url, Id) :-
read_file_to_string("/tmp/url.csv", String, []),
split_string(String, "\n", "", Lines),
find_url_by_id(Id, Lines, Url).
:- initialization(main, main).
:- use_module(library(http/http_dispatch), [http_404/2, http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).
:- use_module(url_to_id, [url_to_id/2]).
% 辅助函数。
reply_by_id(Id, Request) :-
( url_to_id(Url, Id)
-> http_redirect(moved, Url, Request)
; http_404([], Request)).
lengthen_url(Request) :-
http_parameters(Request, [
id(Id, [integer])
]),
reply_by_id(Id, Request).
shorten_url(Request) :-
http_parameters(Request, [
url(Url, [string]) % 因为 url_to_id 中有类型要求。
]),
% 姑且用当前时间戳来作为 ID。
get_time(TimeStamp),
Id is truncate(TimeStamp),
url_to_id(Url, Id),
format('Content-Type: application/json~n~n'),
format('{"id": ~d}', [Id]).
main(_) :-
http_handler('/api/lengthen', lengthen_url, [methods([get])]),
http_handler('/api/shorten', shorten_url, [methods([post])]),
http_server([port(8082)]),
sleep(100000000).