One way. You'll need to adapt it to suit your input.
#! /usr/bin/perl
use strict;
use warnings;
use HTML::Template;
use Data::Dumper;
$Data::Dumper::Indent = 2;
my %record = (
cell_01 => 1,
cell_02 => 2,
cell_03 => 3,
cell_04 => 4,
cell_05 => 5,
cell_06 => 6,
cell_07 => 7,
cell_08 => 8,
cell_09 => 9,
cell_10 => 10,
cell_11 => 11,
cell_12 => 12,
cell_13 => 13,
cell_14 => 14,
cell_15 => 15,
cell_16 => 16,
cell_17 => 17,
cell_18 => 18,
);
my @cells = map{
{cell => $record{$_}}
} sort keys %record;
my (@row, @rows);
my $i = 0;
while ($i < $#cells){
for my $col (0..3){
last if $i > $#cells;
push @row, $cells[$i++];
}
push @rows, {td_loop => [@row]};
@row = ();
}
#print Dumper \@rows;
my $t = HTML::Template->new(filehandle => *DATA)
or die qq{cant load templ};
$t->param(tr_loop => [@rows]);
print $t->output;
__DATA__
<table>
<TMPL_LOOP tr_loop>
<tr>
<TMPL_LOOP td_loop>
<td><TMPL_VAR cell></td>
</TMPL_LOOP>
</tr>
</TMPL_LOOP>
</table>
output (whitespace trimmed)
<table>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>5</td>
<td>6</td>
<td>7</td>
<td>8</td>
</tr>
<tr>
<td>9</td>
<td>10</td>
<td>11</td>
<td>12</td>
</tr>
<tr>
<td>13</td>
<td>14</td>
<td>15</td>
<td>16</td>
</tr>
<tr>
<td>17</td>
<td>18</td>
</tr>
</table>