Quite a while ago I wrote a post on spinning text using PHP to create lots of fresh content from one single block of marked up text. I actually had use for a nested version of this spinning code the other day so set about creating a more efficient version of my previous nested spin code and packed it up in a class wrapper so it would choose whether to use the nested spinner or the flat version dynamically.
Without further ado here is the new PHP Spinner code.
class Spinner
{
# Detects whether to use the nested or flat version of the spinner (costs some speed)
public static function detect($text, $seedPageName = true, $openingConstruct = '{{', $closingConstruct = '}}', $separator = '|')
{
if(preg_match('~'.$openingConstruct.'(?:(?!'.$closingConstruct.').)*'.$openingConstruct.'~s', $text))
{
return self::nested($text, $seedPageName, $openingConstruct, $closingConstruct, $separator);
}
else
{
return self::flat($text, $seedPageName, false, $openingConstruct, $closingConstruct, $separator);
}
}
# The flat version does not allow nested spin blocks, but is much faster (~2x)
public static function flat($text, $seedPageName = true, $calculate = false, $openingConstruct = '{{', $closingConstruct = '}}', $separator = '|')
{
# Choose whether to return the string or the number of permutations
$return = 'text';
if($calculate)
{
$permutations = 1;
$return = 'permutations';
}
# If we have nothing to spin just exit (don't use a regexp)
if(strpos($text, $openingConstruct) === false)
{
return $$return;
}
if(preg_match_all('!'.$openingConstruct.'(.*?)'.$closingConstruct.'!s', $text, $matches))
{
# Optional, always show a particular combination on the page
self::checkSeed($seedPageName);
$find = array();
$replace = array();
foreach($matches[0] as $key => $match)
{
$choices = explode($separator, $matches[1][$key]);
if($calculate)
{
$permutations *= count($choices);
}
else
{
$find[] = $match;
$replace[] = $choices[mt_rand(0, count($choices) - 1)];
}
}
if(!$calculate)
{
# Ensure multiple instances of the same spinning combinations will spin differently
$text = self::str_replace_first($find, $replace, $text);
}
}
return $$return;
}
# The nested version allows nested spin blocks, but is slower
public static function nested($text, $seedPageName = true, $openingConstruct = '{{', $closingConstruct = '}}', $separator = '|')
{
# If we have nothing to spin just exit (don't use a regexp)
if(strpos($text, $openingConstruct) === false)
{
return $text;
}
# Find the first whole match
if(preg_match('!'.$openingConstruct.'(.+?)'.$closingConstruct.'!s', $text, $matches))
{
# Optional, always show a particular combination on the page
self::checkSeed($seedPageName);
# Only take the last block
if(($pos = mb_strrpos($matches[1], $openingConstruct)) !== false)
{
$matches[1] = mb_substr($matches[1], $pos + mb_strlen($openingConstruct));
}
# And spin it
$parts = explode($separator, $matches[1]);
$text = self::str_replace_first($openingConstruct.$matches[1].$closingConstruct, $parts[mt_rand(0, count($parts) - 1)], $text);
# We need to continue until there is nothing left to spin
return self::nested($text, $seedPageName, $openingConstruct, $closingConstruct, $separator);
}
else
{
# If we have nothing to spin just exit
return $text;
}
}
# Similar to str_replace, but only replaces the first instance of the needle
private static function str_replace_first($find, $replace, $string)
{
# Ensure we are dealing with arrays
if(!is_array($find))
{
$find = array($find);
}
if(!is_array($replace))
{
$replace = array($replace);
}
foreach($find as $key => $value)
{
if(($pos = mb_strpos($string, $value)) !== false)
{
# If we have no replacement make it empty
if(!isset($replace[$key]))
{
$replace[$key] = '';
}
$string = mb_substr($string, 0, $pos).$replace[$key].mb_substr($string, $pos + mb_strlen($value));
}
}
return $string;
}
private static function checkSeed($seedPageName)
{
# Don't do the check if we are using random seeds
if($seedPageName)
{
if($seedPageName === true)
{
mt_srand(crc32($_SERVER['REQUEST_URI']));
}
elseif($seedPageName == 'every second')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-m-d-H-i-s')));
}
elseif($seedPageName == 'every minute')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-m-d-H-i')));
}
elseif($seedPageName == 'hourly' OR $seedPageName == 'every hour')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-m-d-H')));
}
elseif($seedPageName == 'daily' OR $seedPageName == 'every day')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-m-d')));
}
elseif($seedPageName == 'weekly' OR $seedPageName == 'every week')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-W')));
}
elseif($seedPageName == 'monthly' OR $seedPageName == 'every month')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y-m')));
}
elseif($seedPageName == 'annually' OR $seedPageName == 'every year')
{
mt_srand(crc32($_SERVER['REQUEST_URI'].date('Y')));
}
elseif(preg_match('!every ([0-9.]+) seconds!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / $matches[1])));
}
elseif(preg_match('!every ([0-9.]+) minutes!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 60))));
}
elseif(preg_match('!every ([0-9.]+) hours!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 3600))));
}
elseif(preg_match('!every ([0-9.]+) days!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 86400))));
}
elseif(preg_match('!every ([0-9.]+) weeks!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 604800))));
}
elseif(preg_match('!every ([0-9.]+) months!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 2620800))));
}
elseif(preg_match('!every ([0-9.]+) years!', $seedPageName, $matches))
{
mt_srand(crc32($_SERVER['REQUEST_URI'].floor(time() / ($matches[1] * 31449600))));
}
else
{
throw new Exception($seedPageName. ' Was not a valid spin time option!');
}
}
}
}
And an example:
$string = '{{The|A}} {{quick|speedy|fast}} {{brown|black|red}} {{fox|wolf}} {{jumped|bounded|hopped|skipped}} over the {{lazy|tired}} {{dog|hound}}';
echo '<p>';
for($i = 1; $i <= 5; $i++)
{
echo Spinner::detect($string, false).'<br />';
// or Spinner::flat($string, false).'<br />';
}
echo '</p>';
Which produces:
The speedy black wolf bounded over the lazy hound
A speedy brown fox skipped over the tired hound
The quick red wolf bounded over the lazy hound
The fast brown fox hopped over the tired dog
The speedy brown fox jumped over the tired dog
and it will work happily as a nested PHP spinner too:
$string = '{{A {{simple|basic}} example|An uncomplicated scenario|The {{simplest|trivial|fundamental|rudimentary}} case|My {{test|invest{{igative|igation}}}} case}} to illustrate the {{function|problem}}';
echo '<p>';
for($i = 1; $i <= 5; $i++)
{
echo Spinner::detect($string, false).'<br />';
// or Spinner::nested($string, false).'<br />';
}
echo '</p>';
Which produces:
A basic example to illustrate the function
My test case to illustrate the problem
The rudimentary case to illustrate the function
An uncomplicated scenario to illustrate the problem
The fundamental case to illustrate the problem
Improvement
The spinner now allows seed page options to be strings such as ‘daily’, ‘every 2.5 hours’ or ‘every 3 weeks’ rather than just true / false so the spun text will automatically update every set period of time. See the checkSeed (bottom) function for allowed values.
Have fun spinning articles! Any comments, suggestions or improvements are welcome.
[...] A new version of this code is available on the Updated PHP Spinner [...]
cool..
this is php spinner that i’m looking for
thanks
Great Code!!! It is guys like you that share things and teach which makes this world a better place! Greed is NOT good!
Thanks again
James
Very nice script. I like it better than the old version. Thanks for sharing it.
Very nice script, it’s what I’m looking for my next project. Thanks
Thanks man, you saved me time, I was going to write one myself. If you want I can share some of my own php code with you, you’ve got my email, let me know.
Hi,
Im having trouble getting the seed page options to work. How do I get it to spin once per month?
If I’ve written it correctly (always fun in WordPress!) then you can achieve that effect using something like:
Spinner::flat($string, 'monthly');
Hi there Paul,
Would you be able to adapt this so that it can be used as an article spinner? I can code a bit of PHP but this is a bit confusing for me.
What I mean is I input the string {hello there mate|hey man|hi dude} etc, it gets it in a $_POST form and then does the spinner stuff and outputs an article/1 spinned form of it.
Sorry if that’s a rubbish explanation, I’ll try and do it myself but I’m not certain I can.
Thanks bro…
[...] ob es da schon Lösungen gibt – man muss ja das Rad nicht neu erfinden. Und tatsächlich: PHP Spinner Updated – Spin articles for SEO hat genau das umgesetzt was ich gesucht [...]
brilliant
it is better than a paid article spinner I’ve got, which can’t properly do its job. this php approach is very good. with my old spinner, the articles where somewhere 20% original when checked with DupeFree duplicate content checker. the article i’ve spun with this script made a 60% uniqueness… impressive and thanks fr sharing your time and work!
i couldn’t get this to spin correctly … the ö seems to be causing the problem … any thoughts?
** Content removed at request of the poster **
I had no problem with that string… I placed it in a variable and then used the following:
echo Spinner::detect($string, false, ‘{‘, ‘}’, ‘|’);
Because you used single braces. Hope it helps!
Hi there, this is an incredibly useful snippet. Are you okay with it being included in something commercial?
Yep, do what you will with it!
Cool thanks!
Thanks for your scripts. You are my god
How to spin a texte from a synonyms database with php ?
I would suggest that you use an existing API for this because they are not simple to construct – the format required does not lend itself to SQL.
For the English language a service like Wordnik could be used to generate the spin blocks with a small amount of extra code. Look into the ‘getRelatedWords’ method using type ‘synonym’ as one of the arguments…
Good luck!
Is there a way to force it to only output unique spins, right now it will output duplicates if you set it to put out more than 5 (providing you dont put enough {{ }} yourself.
Also I can’t figure out what variables its saving each spin to. Because I want to take each spin and do things with it but can’t quite figure that out.
Sorry, I am still learning php.
I would suggest that if you are using it multiple times within a page then you could keep a log of what has already been output. I’d suggest these be hashes (to keep storage space low) and you simple append them to an array after each use and only use the output if its hash isn’t already present in the array. For example (untested!):
$used = array();
while(count($used) < 10)
{
$output = Spinner::detect($string, false);
$hash = sha1($output);
if(!in_array($hash, $used))
{
echo $output;
$used[] = $hash;
}
}
Paul
I’ve been writing a suite of SEO tools recently, and I have to say that your code is one of the most elegant nested-spin solutions I’ve seen.
I’ll now be using it in place of my own nested-spin code!
Kudos… And Thanks – It’s generous of you to give this away.
I’ve bookmarked your site to come back and have a good look around later!
Great piece of recursive coding – just what I was looking for for the last few months but didn’t think it was out there. I was just about to spend quite a few hours writing something similar as I had been putting it off, and then I found your articles – many thanks Paul.
Hey so what I ended up doing because I couldn’t quite figure out the hash code (I am a beginner), I did this to stop the duplicates and to count how many there were. Just thought I would share…
–START CODE–
$article = $_POST['article'];
$outputs = array();
$dupcount = 0;
for($i = 1; $i <= 20; $i++)
{
$article = Spinner::detect($string, false);
if (in_array($article, $outputs)) {
$dupcount++;
}
else {
array_push($outputs, $article);
}
echo $dupcount;
–END CODE–
Superb info. Thanks everyone!
Here’s an idea which I lack the skills to get to work and I hope someone can help with
Using the generously provided code, how would one
1) get the script to fetch a spintax seed from, for example, a sever based text file?
2) get the script / server to output a zip file of the spun text and a link presented for the user on web page?