שליפת מילים מתוך טקסט בTSQL

June 12th 2011 | Posted by משה טייכר

עסקתי לאחרונה בפיתוח מנגנון שמאפשר בצורה קלה לבצע SPLIT למילים מתוך טקסט .

יהיו אנשים שישאלו את עצמם למה זה טוב?
ובכן כאשר נתונה לנו עמודת תגיות אשר המילים בה משורשרות באמצעות פסיק או ברווח.
או לחלופין כאשר יש לנו עמודת תיאור מסוג טקסט ואנו רוצים ליצור טבלת מילות חיפוש לפי מילים.
לדוגמה ראו הטבלה הבאה:

המטרה הסופית היא ליצור טבלת חיפוש שתראה כך:

כאמור אני צריך לבצע מספר פעולות :

  1. לבטל את כל הסימנים המיוחדים (כגון $,%,תווי בקרה וכו)
  2. לבצע גזירה של המילים מתוך הטקסט.
  3. להשמיט מילות יחס (כגון I,AND , THE …)
  4. למנוע חזרה של מילים ברמת נשוא החיפוש GROUP BY)

לצורך ההדגמה אני משתמש בטבלה הבאה:

CREATE TABLE ##MoviesTable ( ID bigint NOT NULL IDENTITY(1, 1), Movies  nvarchar(100) , Stars nvarchar(100))

INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Pirates of the Caribbean: On Stranger Tides',      'Johnny Depp Penélope Cruz Ian McShane');

INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Midnight in Paris',  'Owen Wilson Rachel McAdams  Kathy Bates');

INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Thor',   'Chris Hemsworth  Anthony Hopkins  Natalie Portman');

INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Bridesmaids',  'Kristen Wiig Maya Rudolph  Rose Byrne');

INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Fast Five', 'Vin Diesel Paul Walker Dwayne Johnson')

כדי לבטל סימנים מיוחדים אני משתמש בפונקציות הבאות:

CREATE Function dbo.ReplaceWhiteWords(@Character nvarchar(max))
RETURNS nvarchar(max) As
BEGIN
Set @Character = Replace(@Character, ',', ' ')
Set @Character = Replace(@Character, '!', ' ')
Set @Character = Replace(@Character, '.', ' ')
Set @Character = Replace(@Character, '/', ' ')
Set @Character = Replace(@Character, '(', ' ')
Set @Character = Replace(@Character, ')', ' ')
Set @Character = Replace(@Character, '\', ' ')
Set @Character = Replace(@Character, ';', ' ')
Set @Character = Replace(@Character, '&', ' ')
Set @Character = Replace(@Character, '-', ' ')
Set @Character = Replace(@Character, ',', ' ')
Set @Character = Replace(@Character, '?', ' ')
Set @Character = Replace(@Character, '#', ' ')
Set @Character = Replace(@Character, '$', ' ')
Set @Character = Replace(@Character, '%', ' ')
Set @Character = Replace(@Character, '^', ' ')
Set @Character = Replace(@Character, '&', ' ')
Set @Character = Replace(@Character, '*', ' ')
Set @Character = Replace(@Character, '@', ' ')
Set @Character = Replace(@Character, '+', ' ')
Set @Character = Replace(@Character, '=', ' ')
Set @Character = Replace(@Character, '<', ' ')
Set @Character = Replace(@Character, '>', ' ')
return (dbo.Replace91013(@Character)) –- replace controle character
END

CREATE Function dbo.Replace91013(@Character nvarchar(max))

RETURNS nvarchar(max) As

BEGIN

Set @Character = Replace(@Character,char(9), ' ')
Set @Character = Replace(@Character,char(10), ' ')
Set @Character = Replace(@Character,char(13), ' ')
Set @Character = LTRIM(RTRIM(@Character))
Return (@Character)

End

עד כאן היה פשוט. עכשיו בשלב הבא אנו אמורים לגזור את המילים מתוך הטקסט

אני בחרתי בשיטה שלטעמי פשוטה ונוחה. אני פשוט ממיר את כל הטקסט לXML  וממיר את הרווחים (או הפסיקים במידה ויש) לתגיות XML התוצאה של הפונקציה היא טבלה שמכילה שתי עמודות :

Item, מילה

records(r) זיהוי רשומה

CREATE FUNCTION dbo.Split
(
@delimited nvarchar(max), -- the original text
@delimiter nvarchar(100), -- the delimeter like [,] or [;] or [ ]
@IDRecourd bigint   -- the ID of the recourd
) RETURNS @t TABLE
   (
      val nvarchar(max),
      IDRecourd bigint
   )
AS
BEGIN
declare @xml xml
set @xml = N'<root><r>' + replace(@delimited,@delimiter,'</r><r>') + '</r></root>'  -- convert the text to xml
insert into @t(val,IDRecourd)
select
r.value('.','nvarchar(MAX)') as item, -- convert the word to item from xml
@IDRecourd  -- give the ID record
from @xml.nodes('//root/r') as records(r)
RETURN
END

בשלב הסופי אני יצרתי פרוצדורה אשר משתמשת בפונקציות שתיארתי מקודם ליצירת הטבלה הסופית של המילים ממספר עמודות שונות.

גם כאן בחרתי להשתמש בXML  אבל בשיטה קצת שונה. אני משתמש בביטוי FOR XML PATH בשני אופנים

פעם אחת בחלק הפנימי  כדי לבנות את החלק הפנימי של הXML  (‘Item’) לכל רשומה מקורית. ופעם שנייה לכלל הטבלה.

בסופו של התהליך אני מבצע שליפה מתוך הXML  של המילים וזיהוי הרשומה.

CREATE PROCEDURE dbo.BuildSearchWord_sp1
AS
DECLARE @XML xml
SET @XML =
(     SELECT
   (     SELECT ID, Val AS Word
         FROM dbo.Split(dbo.ReplaceWhiteWords(Movies + ' ' + Stars), ' ', ID)
         WHERE Val <> ''
         AND LEN(Val) >= 2
         FOR XML PATH('Item') , TYPE -- the internal XML
    ) AS 'Items'
FROM #MoviesTable
WHERE
LEN(Movies) > 2
AND LEN(Stars) > 2
FOR XML PATH
)

--SELECT @XML  -- To get the XML opt.
SELECT
T.c.value ('ID[1]', 'bigint') AS ID, -- Select the ID from XML
T.c.value ('Word[1]', 'nvarchar(100)') AS Word -- Select the Word from XML
FROM
@xml.nodes('//row/Items/Item') AS T(c);
GO

להלן התוצאה:

exec BuildSearchWord_sp

ID            Word
1              Pirates
1              of
1              the
1              Caribbean:
1              On
1              Stranger
1              Tides
1              Johnny
1              Depp
1              Penélope
1              Cruz
1              Ian
1              McShane
2              Midnight
2              in
2              Paris
2              Owen
2              Wilson
2              Rachel
2              McAdams
2              Kathy
2              Bates
3              Thor
3              Chris
3              Hemsworth
3              Anthony
3              Hopkins
3              Natalie
3              Portman
4              Bridesmaids
4              Kristen
4              Wiig
4              Maya
4              Rudolph
4              Rose
4              Byrne
5              Fast
5              Five
5              Vin
5              Diesel
5              Paul
5              Walker
5              Dwayne
5              Johnson

לסיכום:

הבאתי כאן דוגמה לשימוש בXML לצורך גזירה פשוטה של מילים מתוך מספר עמודות טקסט.

לטעמי השיטה היא פשוטה יותר ודינמית מאשר שיטות של לולאות

אני משתמש הרבה בXML לדוגמא לצורך שירשור של מחרוזות בתוך רקוסיות , או  כאשר אני רוצה בצורה מהירה לבצע גזירה ו/או מנפולציות  של נתונים המקוננים בתוך טקסט או במספר עמודות שונות .

נתראה ברשומה הבאה,

משה טיכר, DBA אפליקטיבי ומפתח אפליקציות בחברת CoolVision.
בעל וותק של 17 שנה בתחום מסדי נתונים ופיתוח .
ניתן לפנות לאלי במייל: moshe.taicher@gmail.com



מאמרים קשורים

PARSE בצורה מהירה ואלגנטית

אם נקח לדוגמא כתובת IP שהיא בעצם מחרוזת של 4 מספרים ובניהם נקודה ונרצה לחלק אותה לארבע כדי לקבל את כל אחד מחלקי כתובת IP אז בעבר הייתי נדרש לכתוב סקריפט שימצא כל את מיקום הנקודה הראשונה במחרוזת - [...]

שימוש ב-Glogin.SQL ב-SQL*Plus

שימוש ב-Glogin.SQL כולנו עובדים עם SQL*Plus בצורה תכופה יותר או פחות. זהו כלי שמיש והפשוט ביותר להתחבר לשרת Oracle. ברצוני לספר לכם היום טיפ קטן ושימושי שיכול להועיל רבות, כאשר עובדים עם כלי זה מול סביבות [...]

הרשאות – או לא להיות- איך ניתן לקבל הרשאות דרך Role בעת שימוש בקוד PL/SQL

שלום, אלו מכם אשר כותבים קוד בשפת PL/SQL, וודאי מכירים את הסוגיה הקצת מוזרה שבה לעיתים אנחנו נתקלים בהודעות שגיאה עקב בעיות של הרשאות בעת ביצוע פעולות שונות כגון ניסיון ליצירת טבלה מתוך הקוד. הנה [...]

כלום – זה הרבה או קצת ? על Nulls

כלום – זה הרבה או קצת ? נניח שיש לכם עמודה שיש בה "כלומים", או בשפת בסיס הנתונים – Nulls. עכשיו נניח שאתם מבקשים למיין לפי העמודה הזאת, פעם אחת מיון בסדר עולה ופעם אחת מיון בסדר יורד. איפה לדעתכם [...]

Xp_cmdshell

הפרוצדורה הנ"ל שהיא Extended Stored Procedure מאפשרת להריץ Command Lines שהשרת תומך בהם, מתוך SQL Server. קודם כל הכוונה לפקודות הוותיקות המוכרות לנו מימי DOS העליזים- Copy, Dir, Type, Del, MD, RD וכו', ובהמשך לכך Command Lines של [...]

11 Responses to “שליפת מילים מתוך טקסט בTSQL”

עמיאל דייויס | June 14th, 2011 at 12:54 pm
commenter

Very nice post.
I can do it in Oracle with one query:
SQL> CREATE TABLE MoviesTable ( ID number not null, Movies  varchar(100) , Stars
varchar(100));

Table created.

SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2  VALUES (1, ‘Pirates of the Caribbean: On Stranger Tides’,      ‘Johnny Depp
Penélope Cruz Ian McShane’);

1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2  VALUES (2, ‘Midnight in Paris’,  ‘Owen Wilson Rachel McAdams  Kathy Bates’);

1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2  VALUES (3, ‘Thor’,   ‘Chris Hemsworth  Anthony Hopkins  Natalie Portman’);

1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2  VALUES (4, ‘Bridesmaids’,  ‘Kristen Wiig Maya Rudolph  Rose Byrne’);

1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2  VALUES (5, ‘Fast Five’, ‘Vin Diesel Paul Walker Dwayne Johnson’);

1 row created.
SQL> commit;

Commit complete.
SQL> col word for a40
SQL> select * from (
2  select id,
3  regexp_substr(str,
4           ‘\S+’,
5           1,
6           row_number() over(partition by id order by i)
7           ) as word
8  from
9  (select id, Movies || ‘ ‘ || Stars as str from MoviesTable),
10  (select rownum i from dual connect by level <= (
11  select sum(regexp_count(Movies || ‘ ‘ || Stars,
12  ‘\S+’)) from MoviesTable)
13  )) where word is not null;

ID WORD
———- —————————————-
1 Pirates
1 of
1 the
1 Caribbean:
1 On
1 Stranger
1 Tides
1 Johnny
1 Depp
1 Penélope
1 Cruz

ID WORD
———- —————————————-
1 Ian
1 McShane
2 Midnight
2 in
2 Paris
2 Owen
2 Wilson
2 Rachel
2 McAdams
2 Kathy
2 Bates

ID WORD
———- —————————————-
3 Thor
3 Chris
3 Hemsworth
3 Anthony
3 Hopkins
3 Natalie
3 Portman
4 Bridesmaids
4 Kristen
4 Wiig
4 Maya

ID WORD
———- —————————————-
4 Rudolph
4 Rose
4 Byrne
5 Fast
5 Five
5 Vin
5 Diesel
5 Paul
5 Walker
5 Dwayne
5 Johnson

44 rows selected.
Amiel Davis

משה טיכר | June 15th, 2011 at 4:24 am
commenter

אהבתי את הפתרון של עמיאל דייויס ואני בתור אחד שעובד גם בסביבת אורקל וגם בסביבת SQLSERVER יכול רק לתהות למה לא מיישמים בTSQL פונקציות טרוויאליות שקיימות בPLSQL כגון פונקציות ממשפחת REGEXP . או הדבר הכי תמוה בעיני זה למה לא מיישמים את האפשרות של PARTITION WINDOW בפונקציות אנליטיות וזאת עוד במסד נתונים של חברה שניכסה לעצמה את המילה WINDOW כסימן רשום.
בכלל כאשר אני כותב בTSQL לעיתים אני מוצא את עצמי מקלל על הסירבול ועל המבחר המצומצם של TSQL לעומת PLSQL.
אבל איך אומרים “זה מה יש ואם זה צריך להסתדר”

commenter

You can use recursive sql to mimic oracle’s hierarchic sql:
Create the tables and insert rows as mentioned in the article.
Run the following block:

with MoviesTable as (
select ID, Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(Movies, ‘,’, ”), ‘!’, ”)
, ‘.’, ”)
, ‘/’, ”)
, ‘(‘, ”)
, ‘)’, ”)
, ‘\’, ”)
, ‘;’, ”)
, ‘&’, ”)
, ‘-’, ”)
, ‘,’, ”)
, ‘?’, ”)
, ‘#’, ”)
, ‘$’, ”)
, ‘%’, ”)
, ‘^’, ”)
, ‘&’, ”)
, ‘*’, ”)
, ‘@’, ”)
, ‘+’, ”)
, ‘=’, ”)
, ‘<’, ”)
, ‘>’, ”) Movies,
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(Stars, ‘,’, ”), ‘!’, ”)
, ‘.’, ”)
, ‘/’, ”)
, ‘(‘, ”)
, ‘)’, ”)
, ‘\’, ”)
, ‘;’, ”)
, ‘&’, ”)
, ‘-’, ”)
, ‘,’, ”)
, ‘?’, ”)
, ‘#’, ”)
, ‘$’, ”)
, ‘%’, ”)
, ‘^’, ”)
, ‘&’, ”)
, ‘*’, ”)
, ‘@’, ”)
, ‘+’, ”)
, ‘=’, ”)
, ‘<’, ”)
, ‘>’, ”) Stars
from ##MoviesTable)
,MoviesTableMoviesSplit as (
select ID,1 LEVEL,Movies
from MoviesTable
union all
select ID,LEVEL + 1,Movies
from MoviesTableMoviesSplit
where LEVEL < len(Movies))
,MoviesTableStarsSplit as (
select ID,1 LEVEL,Stars
from MoviesTable
union all
select ID,LEVEL + 1,Stars
from MoviesTableStarsSplit
where LEVEL < len(Stars)),
ResultSplit as (
select ‘Movies’ TITLE,ID,LEVEL,
substring(Movies,case when CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL then LEVEL+1 else LEVEL end,
case when CHARINDEX(‘ ‘,Movies,LEVEL+1) <> 0 then CHARINDEX(‘ ‘,Movies,LEVEL+1) else LEN(Movies)+1 end-case when CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL then LEVEL+1 else LEVEL end) Text
from MoviesTableMoviesSplit
where (LEVEL = 1 or CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL)
and CHARINDEX(‘ ‘,Movies,LEVEL+1) <> LEVEL+1
union all
select ‘Stars’ TITLE,ID,LEVEL,
substring(Stars,case when CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL then LEVEL+1 else LEVEL end,
case when CHARINDEX(‘ ‘,Stars,LEVEL+1) <> 0 then CHARINDEX(‘ ‘,Stars,LEVEL+1) else LEN(Stars)+1 end-case when CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL then LEVEL+1 else LEVEL end) Text
from MoviesTableStarsSplit
where (LEVEL = 1 or CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL)
and CHARINDEX(‘ ‘,Stars,LEVEL+1) <> LEVEL+1)
select Text from ResultSplit
order by ID,TITLE,LEVEL
 

משה טיכר | June 26th, 2011 at 7:03 am
commenter

הפתרון של יונה הוא שונה
ואני מוצא בו שתי בעיות האחת היא זמני ביצוע ירודים
הרצתי את הפתרון של יונה לעומת הפתרון שלי והתוצאות הן
הפתרון של יונה
Total execution time 68
Bytes sent from client 5446
Bytes received from server 108086

הפתרון שלי
Total execution time 15

Bytes sent from client 226
Bytes received from server 80367
על פניו הפתרון שלי יותר יעיל.
הבעייה השנייה היא חוסר גמישות במידה ונרצה לחפש על עמודה נוספת לדוגמא director אזי בפתרון שלי כל השינוי יהיה בשתי שורות קוד המודגשות בכוכביות לפי הדוגמה הבאה

CREATE PROCEDURE dbo.BuildSearchWord_sp
AS
DECLARE @XML xml
SET @XML =
(     SELECT
(     SELECT ID, Val AS Word
FROM dbo.Split(dbo.ReplaceWhiteWords(Movies + ‘ ‘ + Stars + ‘ ‘ + Director), ‘ ‘, ID) —****
WHERE Val <> ”
AND LEN(Val) >= 2
FOR XML PATH(‘Item’) , TYPE
) AS ‘Items’
FROM #MoviesTable
WHERE
LEN(Movies) > 2
AND LEN(Stars) > 2
AND LEN(director) > 2  –****
FOR XML PATH
)
SELECT
T.c.value (‘ID[1]‘, ‘bigint’) AS ID,
T.c.value (‘Word[1]‘, ‘nvarchar(100)’) AS Word
FROM
@xml.nodes(‘//row/Items/Item’) AS T(c);
GO

commenter

/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”טבלה רגילה”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-qformat:yes;
mso-style-parent:”";
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin-top:0cm;
mso-para-margin-right:0cm;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0cm;
line-height:115%;
mso-pagination:widow-orphan;
font-size:11.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:”Times New Roman”;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:Arial;
mso-bidi-theme-font:minor-bidi;}

משה תחילה אני אומר שגם אני חושב שפונקציה כמו SPLIT כמובן הייתה אמורה להיות מובנית בשרת. זו אינה הפונקציה היחידה שלטעמי חייבים להטמיע בכל שרת ואני למשל נוהג להטמיע מספר אלמנטים באופן קבוע בכל שרת כמו SPLIT,UNSPLIT, טבלת מספרים ועוד. “אבל” חשוב מאוד שיש כאן זה שאולי הסיבה שלא הוסיפו את זה ב SQL היא שלא ככה מומלץ לעבוד וזו דרך בזבזנית ולא טובה לבצע SPLIT (כתבתי בלוג בעבר עם דוגמאות ובדיקות בנושא וכמוני לפני ואחרי ניתן למצוא עשרות בלוגים בנושא ביצוע SPLIT ב SQL).
ב SQL יש לנו אופציה לעשות שימוש ב CLR. פעולות מסוג אלו הן פעולות מובהקות כמתאימות לשימוש ב CLR. ניתן לבנות בשורת קוד אחת פונקציה פשוטה ב CLR ולהטמיע אותה בשרת. אחרי הכול SPLIT היא פעולה הקיימת בכל שפת דוטנט בפקודה אחת.
תבדוק בלוגים ברשת ותבצע השוואות ותגיע בסיום לבד למסקנה שבשרתSQL הדרך הטובה היא שימוש בפונקצית SPLIT של CLR

commenter

אני לא רואה את התגובה ששמתי עם תגובה ללמה אין את זה ב SQL אבל בכל מקרה הנה קישור להשוואה יפה ושימוש ב CLR פשוט כאן ניתן לראות את הכוח של CLR שכן יש ב SQL http://sqlblog.com/blogs/aaron_bertrand/archive/2010/07/07/splitting-a-list-of-integers-another-roundup.aspx

עמיאל דייויס | July 16th, 2011 at 1:58 pm
commenter

/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”טבלה רגילה”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-qformat:yes;
mso-style-parent:”";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin-top:0in;
mso-para-margin-right:0in;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0in;
line-height:115%;
mso-pagination:widow-orphan;
font-size:11.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:”Times New Roman”;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:Arial;
mso-bidi-theme-font:minor-bidi;}

אכן רעיון מעניין (שימוש ב- clr). כיוון דומה באורקל, יהיה על ידי כתיבת פונקציה חיצונית בשפת C (לחובבי ביצועים מצויינים) או ב- Java. ראוי לציין שהיכולת הזאת קיימת באורקל עוד מהמילניום הקודם…

[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]

[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]

[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]

commenter

Can you please email me the code for this script or please let know me in detail in relation to this script?

Leave a Reply:

Name (required):
Mail (will not be published) (required):
Comment (required):
 
Copyright 2012 Israel Database Portal. Brought to you by Brillix - Israel Leading DBA company. Sponsored by: DBSnaps - Database Video Tutorialss